diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerCodeFixProvider.cs new file mode 100644 index 0000000..1103b4b --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerCodeFixProvider.cs @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev1xxx +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev1007_1008_DictionaryIndexerCodeFixProvider)), Shared] + public sealed class LuceneDev1007_1008_DictionaryIndexerCodeFixProvider : CodeFixProvider + { + private const string TitleReturn = "Use TryGetValue and return default on missing key"; + + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create( + Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType.Id, + Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel == null) return; + + foreach (var diagnostic in context.Diagnostics) + { + var elementAccess = root.FindToken(diagnostic.Location.SourceSpan.Start) + .Parent? + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(e => e.Span.Contains(diagnostic.Location.SourceSpan)); + if (elementAccess == null) + continue; + + // Only handle the "return dict[key];" pattern automatically. + if (elementAccess.Parent is not ReturnStatementSyntax returnStmt + || returnStmt.Expression != elementAccess) + { + continue; + } + + // If the receiver type doesn't expose an accessible TryGetValue method + // (e.g. only via explicit interface implementation), skip — the rewrite would not compile. + if (!HasAccessibleTryGetValue(semanticModel, elementAccess)) + continue; + + context.RegisterCodeFix( + CodeAction.Create( + title: TitleReturn, + createChangedDocument: c => ConvertReturnAsync(context.Document, returnStmt, elementAccess, c), + equivalenceKey: TitleReturn), + diagnostic); + } + } + + private static bool HasAccessibleTryGetValue(SemanticModel semanticModel, ElementAccessExpressionSyntax elementAccess) + { + var receiverType = semanticModel.GetTypeInfo(elementAccess.Expression).Type; + if (receiverType == null) + return false; + + foreach (var member in receiverType.GetMembers("TryGetValue")) + { + if (member is not IMethodSymbol method) + continue; + if (method.IsStatic) + continue; + if (method.DeclaredAccessibility != Accessibility.Public) + continue; + if (method.Parameters.Length != 2) + continue; + if (method.Parameters[1].RefKind != RefKind.Out) + continue; + return true; + } + + return false; + } + + private static async Task ConvertReturnAsync( + Document document, + ReturnStatementSyntax returnStmt, + ElementAccessExpressionSyntax elementAccess, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) return document; + + var receiver = elementAccess.Expression; + var keyArg = elementAccess.ArgumentList.Arguments.FirstOrDefault(); + if (keyArg == null) return document; + + var outName = PickLocalName(returnStmt); + + // receiver.TryGetValue(key, out var ) + var tryGetValueInvocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + receiver.WithoutTrivia(), + SyntaxFactory.IdentifierName("TryGetValue")), + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] + { + keyArg.WithoutTrivia(), + SyntaxFactory.Argument( + SyntaxFactory.DeclarationExpression( + SyntaxFactory.IdentifierName( + SyntaxFactory.Identifier("var")), + SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(outName)))) + .WithRefOrOutKeyword(SyntaxFactory.Token(SyntaxKind.OutKeyword)) + }))); + + // tryGetValueInvocation ? : default + var ternary = SyntaxFactory.ConditionalExpression( + tryGetValueInvocation, + SyntaxFactory.IdentifierName(outName), + SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression, + SyntaxFactory.Token(SyntaxKind.DefaultKeyword))); + + var newReturn = returnStmt.WithExpression(ternary).WithAdditionalAnnotations(Formatter.Annotation); + + var newRoot = root.ReplaceNode(returnStmt, newReturn); + return document.WithSyntaxRoot(newRoot); + } + + private static string PickLocalName(SyntaxNode context) + { + // Avoid collisions with identifiers in the enclosing member. + var member = context.AncestorsAndSelf().OfType().FirstOrDefault(); + var names = member == null + ? ImmutableHashSet.Empty + : member.DescendantTokens() + .Where(t => t.IsKind(SyntaxKind.IdentifierToken)) + .Select(t => t.ValueText) + .ToImmutableHashSet(); + + if (!names.Contains("value")) + return "value"; + for (int i = 1; i < 100; i++) + { + var candidate = "value" + i; + if (!names.Contains(candidate)) + return candidate; + } + return "value"; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerSample.cs new file mode 100644 index 0000000..66ede90 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerSample.cs @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System.Collections.Generic; + +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev1xxx; + +public class LuceneDev1007_1008_DictionaryIndexerSample +{ + public int GetIntValue(IDictionary dict, string key) + { + // LuceneDev1007 (value-type value): indexer may throw KeyNotFoundException. + return dict[key]; + } + + public string GetStringValue(IDictionary dict, string key) + { + // LuceneDev1008 (reference-type value): indexer may throw KeyNotFoundException. + return dict[key]; + } + + public void ReadOnlyUsage(IReadOnlyDictionary dict, string key) + { + // LuceneDev1008: also applies to IReadOnlyDictionary. + var value = dict[key]; + } + + public void ConcreteDictionaryUsage(Dictionary dict, string key) + { + // LuceneDev1008: Dictionary implements IDictionary. + var value = dict[key]; + } + + public void AssignmentIsFine(Dictionary dict, string key) + { + // No diagnostic: indexer setter does not throw. + dict[key] = 42; + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6000_NonGenericDictionaryIndexerSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6000_NonGenericDictionaryIndexerSample.cs new file mode 100644 index 0000000..18636c0 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6000_NonGenericDictionaryIndexerSample.cs @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System.Collections; + +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx; + +public class LuceneDev6000_NonGenericDictionaryIndexerSample +{ + public object? GetValue(IDictionary dict, object key) + { + // LuceneDev6000 (Info): non-generic IDictionary indexer may return null for missing keys. + return dict[key]; + } + + public object? GetValueFromHashtable(Hashtable table, object key) + { + // LuceneDev6000: Hashtable implements non-generic IDictionary. + return table[key]; + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerAnalyzer.cs new file mode 100644 index 0000000..e4d9b19 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev1xxx/LuceneDev1007_1008_DictionaryIndexerAnalyzer.cs @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System.Collections.Immutable; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev1xxx +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LuceneDev1007_1008_DictionaryIndexerAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create( + Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType, + Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeElementAccess, SyntaxKind.ElementAccessExpression); + } + + private static void AnalyzeElementAccess(SyntaxNodeAnalysisContext ctx) + { + var elementAccess = (ElementAccessExpressionSyntax)ctx.Node; + + // Skip assignment targets (setter usage does not throw). + if (IsAssignmentTarget(elementAccess)) + return; + + var symbolInfo = ctx.SemanticModel.GetSymbolInfo(elementAccess, ctx.CancellationToken); + var property = symbolInfo.Symbol as IPropertySymbol; + if (property == null || !property.IsIndexer) + return; + + var containing = property.ContainingType; + if (containing == null) + return; + + if (!DictionaryTypeHelper.IsGenericDictionaryIndexer(property, containing, out var valueType)) + return; + + var receiverText = elementAccess.Expression.ToString(); + var keyText = elementAccess.ArgumentList.ToString(); + var display = receiverText + keyText; + + var descriptor = IsValueTypeForDiagnostic(valueType!) + ? Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType + : Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType; + + ctx.ReportDiagnostic(Diagnostic.Create(descriptor, elementAccess.GetLocation(), display)); + } + + private static bool IsAssignmentTarget(ElementAccessExpressionSyntax elementAccess) + { + // dict[key] = value -> skip + if (elementAccess.Parent is AssignmentExpressionSyntax assignment + && assignment.Left == elementAccess + && assignment.IsKind(SyntaxKind.SimpleAssignmentExpression)) + { + return true; + } + return false; + } + + private static bool IsValueTypeForDiagnostic(ITypeSymbol valueType) + { + // Unconstrained type parameters: treat as reference-like (safer — null check may apply). + if (valueType is ITypeParameterSymbol tp) + { + if (tp.HasValueTypeConstraint) + return true; + return false; + } + return valueType.IsValueType; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6000_NonGenericDictionaryIndexerAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6000_NonGenericDictionaryIndexerAnalyzer.cs new file mode 100644 index 0000000..4029980 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6000_NonGenericDictionaryIndexerAnalyzer.cs @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System.Collections.Immutable; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LuceneDev6000_NonGenericDictionaryIndexerAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(Descriptors.LuceneDev6000_NonGenericDictionaryIndexer); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeElementAccess, SyntaxKind.ElementAccessExpression); + } + + private static void AnalyzeElementAccess(SyntaxNodeAnalysisContext ctx) + { + var elementAccess = (ElementAccessExpressionSyntax)ctx.Node; + + if (elementAccess.Parent is AssignmentExpressionSyntax assignment + && assignment.Left == elementAccess + && assignment.IsKind(SyntaxKind.SimpleAssignmentExpression)) + { + return; + } + + var symbolInfo = ctx.SemanticModel.GetSymbolInfo(elementAccess, ctx.CancellationToken); + var property = symbolInfo.Symbol as IPropertySymbol; + if (property == null || !property.IsIndexer) + return; + + var containing = property.ContainingType; + if (containing == null) + return; + + if (!DictionaryTypeHelper.IsNonGenericDictionaryIndexer(property, containing)) + return; + + var display = elementAccess.Expression.ToString() + elementAccess.ArgumentList.ToString(); + ctx.ReportDiagnostic(Diagnostic.Create( + Descriptors.LuceneDev6000_NonGenericDictionaryIndexer, + elementAccess.GetLocation(), + display)); + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx index f17eceb..53dfc08 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -219,6 +219,39 @@ under the License. The format-able message the diagnostic displays. + + + Generic Dictionary indexer should not be used to retrieve values (value type) + + + Reading values from IDictionary<TKey, TValue> or IReadOnlyDictionary<TKey, TValue> via the indexer throws KeyNotFoundException when the key is missing. Use TryGetValue instead. + + + Generic Dictionary indexer '{0}' may throw KeyNotFoundException. Use TryGetValue instead. + + + + + Generic Dictionary indexer should not be used to retrieve values (reference type) + + + Reading values from IDictionary<TKey, TValue> or IReadOnlyDictionary<TKey, TValue> via the indexer throws KeyNotFoundException when the key is missing. Use TryGetValue instead, and check the out value for null before using it. + + + Generic Dictionary indexer '{0}' may throw KeyNotFoundException. Use TryGetValue and check the value for null. + + + + + Non-generic IDictionary indexer may return null + + + The non-generic IDictionary indexer returns null for missing keys rather than throwing. Review these usages to ensure the value is checked for null before use. + + + Non-generic IDictionary indexer '{0}' may return null for missing keys. Verify the value is checked for null before use. + + Missing StringComparison argument diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs index 7fa199c..b2ae98e 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev1xxx.cs @@ -77,5 +77,19 @@ public static partial class Descriptors Design, Warning ); + + public static readonly DiagnosticDescriptor LuceneDev1007_GenericDictionaryIndexerValueType = + Diagnostic( + "LuceneDev1007", + Design, + Warning + ); + + public static readonly DiagnosticDescriptor LuceneDev1008_GenericDictionaryIndexerReferenceType = + Diagnostic( + "LuceneDev1008", + Design, + Warning + ); } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs index a6734f3..5aa9e27 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs @@ -29,6 +29,14 @@ public static partial class Descriptors // and will report RS2002 warnings if it cannot read the DiagnosticDescriptor // instance through a field. + // 6000: Non-generic IDictionary indexer usage — may return null for missing keys + public static readonly DiagnosticDescriptor LuceneDev6000_NonGenericDictionaryIndexer = + Diagnostic( + "LuceneDev6000", + Usage, + Info + ); + // 6001: Missing StringComparison argument on String overload public static readonly DiagnosticDescriptor LuceneDev6001_MissingStringComparison = Diagnostic( diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/DictionaryTypeHelper.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/DictionaryTypeHelper.cs new file mode 100644 index 0000000..7547434 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/DictionaryTypeHelper.cs @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using Microsoft.CodeAnalysis; + +namespace Lucene.Net.CodeAnalysis.Dev.Utility +{ + internal static class DictionaryTypeHelper + { + private const string GenericIDictionary = "System.Collections.Generic.IDictionary`2"; + private const string GenericIReadOnlyDictionary = "System.Collections.Generic.IReadOnlyDictionary`2"; + private const string NonGenericIDictionary = "System.Collections.IDictionary"; + + /// + /// Returns true when is the indexer declared by (or implementing) + /// IDictionary<TKey, TValue> or IReadOnlyDictionary<TKey, TValue> on a type + /// that implements one of those interfaces. + /// + public static bool IsGenericDictionaryIndexer(IPropertySymbol indexer, INamedTypeSymbol containingType, out ITypeSymbol? valueType) + { + valueType = null; + + // Must take a single key parameter. (The non-generic IDictionary.this[object] variant is handled elsewhere.) + if (indexer.Parameters.Length != 1) + return false; + + // If the containing type is itself IDictionary<,> or IReadOnlyDictionary<,>, this is direct. + if (containingType.IsGenericType + && (MatchesConstructedFrom(containingType, GenericIDictionary) + || MatchesConstructedFrom(containingType, GenericIReadOnlyDictionary))) + { + valueType = containingType.TypeArguments[1]; + return true; + } + + // Otherwise, containing type must implement one of the generic dictionary interfaces, + // and the parameter type must match TKey of an implemented interface. + foreach (var iface in containingType.AllInterfaces) + { + if (!iface.IsGenericType) + continue; + + var matches = MatchesConstructedFrom(iface, GenericIDictionary) + || MatchesConstructedFrom(iface, GenericIReadOnlyDictionary); + if (!matches) + continue; + + var tkey = iface.TypeArguments[0]; + var tvalue = iface.TypeArguments[1]; + + if (SymbolEqualityComparer.Default.Equals(indexer.Parameters[0].Type, tkey)) + { + valueType = tvalue; + return true; + } + } + + return false; + } + + /// + /// Returns true when is the non-generic IDictionary indexer + /// (takes , returns ) declared by or implemented on a type + /// that implements System.Collections.IDictionary. + /// + public static bool IsNonGenericDictionaryIndexer(IPropertySymbol indexer, INamedTypeSymbol containingType) + { + if (indexer.Parameters.Length != 1) + return false; + + // The non-generic IDictionary indexer returns object and takes object. + if (indexer.Parameters[0].Type.SpecialType != SpecialType.System_Object) + return false; + if (indexer.Type.SpecialType != SpecialType.System_Object) + return false; + + if (containingType.ToDisplayString() == NonGenericIDictionary) + return true; + + foreach (var iface in containingType.AllInterfaces) + { + if (iface.ToDisplayString() == NonGenericIDictionary) + return true; + } + + return false; + } + + private static bool MatchesConstructedFrom(INamedTypeSymbol type, string metadataName) + { + var constructedFrom = type.ConstructedFrom ?? type; + return constructedFrom.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .Equals("global::" + StripArity(metadataName), System.StringComparison.Ordinal) + || MetadataNameEquals(constructedFrom, metadataName); + } + + private static string StripArity(string metadataName) + { + var backtick = metadataName.IndexOf('`'); + return backtick < 0 ? metadataName : metadataName.Substring(0, backtick); + } + + private static bool MetadataNameEquals(INamedTypeSymbol type, string metadataName) + { + var ns = type.ContainingNamespace?.ToDisplayString() ?? string.Empty; + var full = string.IsNullOrEmpty(ns) ? type.MetadataName : ns + "." + type.MetadataName; + return full == metadataName; + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1007_1008_DictionaryIndexerCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1007_1008_DictionaryIndexerCodeFixProvider.cs new file mode 100644 index 0000000..b20f93f --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev1xxx/TestLuceneDev1007_1008_DictionaryIndexerCodeFixProvider.cs @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev1xxx; +using Lucene.Net.CodeAnalysis.Dev.LuceneDev1xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes +{ + public class TestLuceneDev1007_1008_DictionaryIndexerCodeFixProvider + { + [Test] + public async Task TestFix_Return_ReferenceType() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public string M(IDictionary dict, string key) + { + return dict[key]; + } +}"; + + var fixedCode = @" +using System.Collections.Generic; + +public class Sample +{ + public string M(IDictionary dict, string key) + { + return dict.TryGetValue(key, out var value) ? value : default; + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType.MessageFormat) + .WithArguments("dict[key]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1007_1008_DictionaryIndexerAnalyzer(), + () => new LuceneDev1007_1008_DictionaryIndexerCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_Return_ValueType() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public int M(IDictionary dict, string key) + { + return dict[key]; + } +}"; + + var fixedCode = @" +using System.Collections.Generic; + +public class Sample +{ + public int M(IDictionary dict, string key) + { + return dict.TryGetValue(key, out var value) ? value : default; + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType.MessageFormat) + .WithArguments("dict[key]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1007_1008_DictionaryIndexerAnalyzer(), + () => new LuceneDev1007_1008_DictionaryIndexerCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_Return_PicksUniqueLocalName_WhenValueIsInScope() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public int M(IDictionary dict, string key) + { + int value = 42; + if (value > 0) + { + return dict[key]; + } + return value; + } +}"; + + var fixedCode = @" +using System.Collections.Generic; + +public class Sample +{ + public int M(IDictionary dict, string key) + { + int value = 42; + if (value > 0) + { + return dict.TryGetValue(key, out var value1) ? value1 : default; + } + return value; + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType.MessageFormat) + .WithArguments("dict[key]") + .WithLocation("/0/Test0.cs", line: 11, column: 20); + + var test = new InjectableCodeFixTest( + () => new LuceneDev1007_1008_DictionaryIndexerAnalyzer(), + () => new LuceneDev1007_1008_DictionaryIndexerCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1007_1008_DictionaryIndexerAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1007_1008_DictionaryIndexerAnalyzer.cs new file mode 100644 index 0000000..74bbc1f --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev1xxx/TestLuceneDev1007_1008_DictionaryIndexerAnalyzer.cs @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev1xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev1xxx +{ + [TestFixture] + public class TestLuceneDev1007_1008_DictionaryIndexerAnalyzer + { + [Test] + public async Task Detects_IDictionary_ValueType_Reports1007() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public int M(IDictionary dict) + { + return dict[""key""]; + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType.MessageFormat) + .WithArguments("dict[\"key\"]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1007_1008_DictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + await test.RunAsync(); + } + + [Test] + public async Task Detects_IDictionary_ReferenceType_Reports1008() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public string M(IDictionary dict) + { + return dict[""key""]; + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType.MessageFormat) + .WithArguments("dict[\"key\"]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1007_1008_DictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + await test.RunAsync(); + } + + [Test] + public async Task Detects_IReadOnlyDictionary_Reports1008() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public string M(IReadOnlyDictionary dict) + { + return dict[""key""]; + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType.MessageFormat) + .WithArguments("dict[\"key\"]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1007_1008_DictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + await test.RunAsync(); + } + + [Test] + public async Task Detects_ConcreteDictionary_Reports1007() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public int M(Dictionary dict) + { + return dict[""key""]; + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType.MessageFormat) + .WithArguments("dict[\"key\"]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1007_1008_DictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + await test.RunAsync(); + } + + [Test] + public async Task NoDiagnostic_On_SetterAssignment() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public void M(Dictionary dict) + { + dict[""key""] = 42; + } +}"; + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1007_1008_DictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } + }; + await test.RunAsync(); + } + + [Test] + public async Task NoDiagnostic_On_NonDictionaryIndexer() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public int M(List list) + { + return list[0]; + } +}"; + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev1007_1008_DictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } + }; + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6000_NonGenericDictionaryIndexerAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6000_NonGenericDictionaryIndexerAnalyzer.cs new file mode 100644 index 0000000..d8f8794 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6000_NonGenericDictionaryIndexerAnalyzer.cs @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx +{ + [TestFixture] + public class TestLuceneDev6000_NonGenericDictionaryIndexerAnalyzer + { + [Test] + public async Task Detects_NonGenericIDictionary_Indexer() + { + var testCode = @" +using System.Collections; + +public class Sample +{ + public object M(IDictionary dict) + { + return dict[""key""]; + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev6000_NonGenericDictionaryIndexer) + .WithSeverity(DiagnosticSeverity.Info) + .WithMessageFormat(Descriptors.LuceneDev6000_NonGenericDictionaryIndexer.MessageFormat) + .WithArguments("dict[\"key\"]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6000_NonGenericDictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + await test.RunAsync(); + } + + [Test] + public async Task Detects_Hashtable_Indexer() + { + var testCode = @" +using System.Collections; + +public class Sample +{ + public object M(Hashtable table) + { + return table[""key""]; + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev6000_NonGenericDictionaryIndexer) + .WithSeverity(DiagnosticSeverity.Info) + .WithMessageFormat(Descriptors.LuceneDev6000_NonGenericDictionaryIndexer.MessageFormat) + .WithArguments("table[\"key\"]") + .WithLocation("/0/Test0.cs", line: 8, column: 16); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6000_NonGenericDictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + await test.RunAsync(); + } + + [Test] + public async Task NoDiagnostic_On_Generic_Dictionary() + { + var testCode = @" +using System.Collections.Generic; + +public class Sample +{ + public string M(Dictionary dict) + { + return dict[""key""]; + } +}"; + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6000_NonGenericDictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } + }; + await test.RunAsync(); + } + + [Test] + public async Task NoDiagnostic_On_SetterAssignment() + { + var testCode = @" +using System.Collections; + +public class Sample +{ + public void M(IDictionary dict) + { + dict[""key""] = ""value""; + } +}"; + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6000_NonGenericDictionaryIndexerAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } + }; + await test.RunAsync(); + } + } +}