Skip to content

Commit 9e14834

Browse files
paulirwinclaude
andcommitted
Implement LuceneDev1007, 1008, 6000 Analyzers & CodeFix with Unit Tests
Adds diagnostics for dictionary indexer usage ported from Java: - LuceneDev1007 (Design/Warning): generic IDictionary<,> / IReadOnlyDictionary<,> indexer read with a value-type value — may throw KeyNotFoundException. - LuceneDev1008 (Design/Warning): same, but reference-type value; fix should also null-check the result. - LuceneDev6000 (Usage/Info): non-generic IDictionary indexer read, which returns null for missing keys and should be reviewed. Includes a code fix for LuceneDev1007/1008 that rewrites the common `return dict[key];` pattern into `return dict.TryGetValue(key, out var value) ? value : default;`. Closes #1168. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 614bad6 commit 9e14834

12 files changed

Lines changed: 1013 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
using System.Collections.Immutable;
21+
using System.Composition;
22+
using System.Linq;
23+
using System.Threading;
24+
using System.Threading.Tasks;
25+
using Lucene.Net.CodeAnalysis.Dev.Utility;
26+
using Microsoft.CodeAnalysis;
27+
using Microsoft.CodeAnalysis.CodeActions;
28+
using Microsoft.CodeAnalysis.CodeFixes;
29+
using Microsoft.CodeAnalysis.CSharp;
30+
using Microsoft.CodeAnalysis.CSharp.Syntax;
31+
using Microsoft.CodeAnalysis.Formatting;
32+
33+
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev1xxx
34+
{
35+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev1007_1008_DictionaryIndexerCodeFixProvider)), Shared]
36+
public sealed class LuceneDev1007_1008_DictionaryIndexerCodeFixProvider : CodeFixProvider
37+
{
38+
private const string TitleReturn = "Use TryGetValue and return default on missing key";
39+
40+
public override ImmutableArray<string> FixableDiagnosticIds =>
41+
ImmutableArray.Create(
42+
Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType.Id,
43+
Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType.Id);
44+
45+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
46+
47+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
48+
{
49+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
50+
if (root == null) return;
51+
52+
foreach (var diagnostic in context.Diagnostics)
53+
{
54+
var elementAccess = root.FindToken(diagnostic.Location.SourceSpan.Start)
55+
.Parent?
56+
.AncestorsAndSelf()
57+
.OfType<ElementAccessExpressionSyntax>()
58+
.FirstOrDefault(e => e.Span.Contains(diagnostic.Location.SourceSpan));
59+
if (elementAccess == null)
60+
continue;
61+
62+
// Only handle the "return dict[key];" pattern automatically.
63+
if (elementAccess.Parent is not ReturnStatementSyntax returnStmt
64+
|| returnStmt.Expression != elementAccess)
65+
{
66+
continue;
67+
}
68+
69+
context.RegisterCodeFix(
70+
CodeAction.Create(
71+
title: TitleReturn,
72+
createChangedDocument: c => ConvertReturnAsync(context.Document, returnStmt, elementAccess, c),
73+
equivalenceKey: TitleReturn),
74+
diagnostic);
75+
}
76+
}
77+
78+
private static async Task<Document> ConvertReturnAsync(
79+
Document document,
80+
ReturnStatementSyntax returnStmt,
81+
ElementAccessExpressionSyntax elementAccess,
82+
CancellationToken cancellationToken)
83+
{
84+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
85+
if (root == null) return document;
86+
87+
var receiver = elementAccess.Expression;
88+
var keyArg = elementAccess.ArgumentList.Arguments.FirstOrDefault();
89+
if (keyArg == null) return document;
90+
91+
var outName = PickLocalName(returnStmt);
92+
93+
// receiver.TryGetValue(key, out var <outName>)
94+
var tryGetValueInvocation = SyntaxFactory.InvocationExpression(
95+
SyntaxFactory.MemberAccessExpression(
96+
SyntaxKind.SimpleMemberAccessExpression,
97+
receiver.WithoutTrivia(),
98+
SyntaxFactory.IdentifierName("TryGetValue")),
99+
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[]
100+
{
101+
keyArg.WithoutTrivia(),
102+
SyntaxFactory.Argument(
103+
SyntaxFactory.DeclarationExpression(
104+
SyntaxFactory.IdentifierName(
105+
SyntaxFactory.Identifier("var")),
106+
SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(outName))))
107+
.WithRefOrOutKeyword(SyntaxFactory.Token(SyntaxKind.OutKeyword))
108+
})));
109+
110+
// tryGetValueInvocation ? <outName> : default
111+
var ternary = SyntaxFactory.ConditionalExpression(
112+
tryGetValueInvocation,
113+
SyntaxFactory.IdentifierName(outName),
114+
SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression,
115+
SyntaxFactory.Token(SyntaxKind.DefaultKeyword)));
116+
117+
var newReturn = returnStmt.WithExpression(ternary).WithAdditionalAnnotations(Formatter.Annotation);
118+
119+
var newRoot = root.ReplaceNode(returnStmt, newReturn);
120+
return document.WithSyntaxRoot(newRoot);
121+
}
122+
123+
private static string PickLocalName(SyntaxNode context)
124+
{
125+
// Avoid collisions with identifiers in the enclosing member.
126+
var member = context.AncestorsAndSelf().OfType<MemberDeclarationSyntax>().FirstOrDefault();
127+
var names = member == null
128+
? ImmutableHashSet<string>.Empty
129+
: member.DescendantTokens()
130+
.Where(t => t.IsKind(SyntaxKind.IdentifierToken))
131+
.Select(t => t.ValueText)
132+
.ToImmutableHashSet();
133+
134+
if (!names.Contains("value"))
135+
return "value";
136+
for (int i = 1; i < 100; i++)
137+
{
138+
var candidate = "value" + i;
139+
if (!names.Contains(candidate))
140+
return candidate;
141+
}
142+
return "value";
143+
}
144+
}
145+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
using System.Collections.Generic;
21+
22+
namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev1xxx;
23+
24+
public class LuceneDev1007_1008_DictionaryIndexerSample
25+
{
26+
public int GetIntValue(IDictionary<string, int> dict, string key)
27+
{
28+
// LuceneDev1007 (value-type value): indexer may throw KeyNotFoundException.
29+
return dict[key];
30+
}
31+
32+
public string GetStringValue(IDictionary<string, string> dict, string key)
33+
{
34+
// LuceneDev1008 (reference-type value): indexer may throw KeyNotFoundException.
35+
return dict[key];
36+
}
37+
38+
public void ReadOnlyUsage(IReadOnlyDictionary<string, string> dict, string key)
39+
{
40+
// LuceneDev1008: also applies to IReadOnlyDictionary<TKey, TValue>.
41+
var value = dict[key];
42+
}
43+
44+
public void ConcreteDictionaryUsage(Dictionary<string, string> dict, string key)
45+
{
46+
// LuceneDev1008: Dictionary<TKey, TValue> implements IDictionary<TKey, TValue>.
47+
var value = dict[key];
48+
}
49+
50+
public void AssignmentIsFine(Dictionary<string, int> dict, string key)
51+
{
52+
// No diagnostic: indexer setter does not throw.
53+
dict[key] = 42;
54+
}
55+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
using System.Collections;
21+
22+
namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx;
23+
24+
public class LuceneDev6000_NonGenericDictionaryIndexerSample
25+
{
26+
public object? GetValue(IDictionary dict, object key)
27+
{
28+
// LuceneDev6000 (Info): non-generic IDictionary indexer may return null for missing keys.
29+
return dict[key];
30+
}
31+
32+
public object? GetValueFromHashtable(Hashtable table, object key)
33+
{
34+
// LuceneDev6000: Hashtable implements non-generic IDictionary.
35+
return table[key];
36+
}
37+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
using System.Collections.Immutable;
21+
using Lucene.Net.CodeAnalysis.Dev.Utility;
22+
using Microsoft.CodeAnalysis;
23+
using Microsoft.CodeAnalysis.CSharp;
24+
using Microsoft.CodeAnalysis.CSharp.Syntax;
25+
using Microsoft.CodeAnalysis.Diagnostics;
26+
27+
namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev1xxx
28+
{
29+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
30+
public sealed class LuceneDev1007_1008_DictionaryIndexerAnalyzer : DiagnosticAnalyzer
31+
{
32+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
33+
ImmutableArray.Create(
34+
Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType,
35+
Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType);
36+
37+
public override void Initialize(AnalysisContext context)
38+
{
39+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
40+
context.EnableConcurrentExecution();
41+
context.RegisterSyntaxNodeAction(AnalyzeElementAccess, SyntaxKind.ElementAccessExpression);
42+
}
43+
44+
private static void AnalyzeElementAccess(SyntaxNodeAnalysisContext ctx)
45+
{
46+
var elementAccess = (ElementAccessExpressionSyntax)ctx.Node;
47+
48+
// Skip assignment targets (setter usage does not throw).
49+
if (IsAssignmentTarget(elementAccess))
50+
return;
51+
52+
var symbolInfo = ctx.SemanticModel.GetSymbolInfo(elementAccess, ctx.CancellationToken);
53+
var property = symbolInfo.Symbol as IPropertySymbol;
54+
if (property == null || !property.IsIndexer)
55+
return;
56+
57+
var containing = property.ContainingType;
58+
if (containing == null)
59+
return;
60+
61+
if (!DictionaryTypeHelper.IsGenericDictionaryIndexer(property, containing, out var valueType))
62+
return;
63+
64+
var receiverText = elementAccess.Expression.ToString();
65+
var keyText = elementAccess.ArgumentList.ToString();
66+
var display = receiverText + keyText;
67+
68+
var descriptor = IsValueTypeForDiagnostic(valueType!)
69+
? Descriptors.LuceneDev1007_GenericDictionaryIndexerValueType
70+
: Descriptors.LuceneDev1008_GenericDictionaryIndexerReferenceType;
71+
72+
ctx.ReportDiagnostic(Diagnostic.Create(descriptor, elementAccess.GetLocation(), display));
73+
}
74+
75+
private static bool IsAssignmentTarget(ElementAccessExpressionSyntax elementAccess)
76+
{
77+
// dict[key] = value -> skip
78+
if (elementAccess.Parent is AssignmentExpressionSyntax assignment
79+
&& assignment.Left == elementAccess
80+
&& assignment.IsKind(SyntaxKind.SimpleAssignmentExpression))
81+
{
82+
return true;
83+
}
84+
return false;
85+
}
86+
87+
private static bool IsValueTypeForDiagnostic(ITypeSymbol valueType)
88+
{
89+
// Unconstrained type parameters: treat as reference-like (safer — null check may apply).
90+
if (valueType is ITypeParameterSymbol tp)
91+
{
92+
if (tp.HasValueTypeConstraint)
93+
return true;
94+
return false;
95+
}
96+
return valueType.IsValueType;
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)