2
// VariableDeclaredWideScopeIssue.cs
5
// Simon Lindgren <simon.n.lindgren@gmail.com>
7
// Copyright (c) 2012 Simon Lindgren
9
// Permission is hereby granted, free of charge, to any person obtaining a copy
10
// of this software and associated documentation files (the "Software"), to deal
11
// in the Software without restriction, including without limitation the rights
12
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
// copies of the Software, and to permit persons to whom the Software is
14
// furnished to do so, subject to the following conditions:
16
// The above copyright notice and this permission notice shall be included in
17
// all copies or substantial portions of the Software.
19
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
using System.Collections.Generic;
29
using ICSharpCode.NRefactory.Semantics;
30
using ICSharpCode.NRefactory.TypeSystem;
32
namespace ICSharpCode.NRefactory.CSharp.Refactoring
34
[IssueDescription("The variable can be declared in a nested scope",
35
Description = "Highlights variables that can be declared in a nested scope.",
36
Category = IssueCategories.Opportunities,
37
Severity = Severity.Suggestion)]
38
public class VariableDeclaredInWideScopeIssue : ICodeIssueProvider
40
#region ICodeIssueProvider implementation
41
public IEnumerable<CodeIssue> GetIssues(BaseRefactoringContext context)
43
return new GatherVisitor(context, this).GetIssues();
47
class GatherVisitor : GatherVisitorBase<VariableDeclaredInWideScopeIssue>
49
readonly BaseRefactoringContext context;
51
public GatherVisitor(BaseRefactoringContext context, VariableDeclaredInWideScopeIssue issueProvider) : base (context, issueProvider)
53
this.context = context;
56
static readonly IList<Type> moveTargetBlacklist = new List<Type> {
57
typeof(WhileStatement),
58
typeof(ForeachStatement),
60
typeof(DoWhileStatement),
61
typeof(TryCatchStatement),
62
typeof(AnonymousMethodExpression),
63
typeof(LambdaExpression),
67
class CheckInitializer : DepthFirstAstVisitor
74
public CheckInitializer()
79
public override void VisitInvocationExpression(InvocationExpression invocationExpression)
81
base.VisitInvocationExpression(invocationExpression);
86
bool CheckForInvocations(Expression initializer)
88
var visitor = new CheckInitializer();
89
initializer.AcceptVisitor(visitor);
90
return visitor.IsValid;
93
public override void VisitVariableDeclarationStatement(VariableDeclarationStatement variableDeclarationStatement)
95
base.VisitVariableDeclarationStatement(variableDeclarationStatement);
97
var rootNode = variableDeclarationStatement.Parent as BlockStatement;
99
// We are somewhere weird, like a the ResourceAquisition of a using statement
102
// TODO: Handle declarations with more than one variable?
103
if (variableDeclarationStatement.Variables.Count > 1)
106
var variableInitializer = variableDeclarationStatement.Variables.First();
107
var identifiers = GetIdentifiers(rootNode.Descendants, variableInitializer.Name).ToList();
109
if (identifiers.Count == 0)
110
// variable is not used
113
if (!CheckForInvocations(variableInitializer.Initializer))
116
AstNode deepestCommonAncestor = GetDeepestCommonAncestor(rootNode, identifiers);
117
var path = GetPath(rootNode, deepestCommonAncestor);
119
// The node that will follow the moved declaration statement
120
AstNode anchorNode = GetInitialAnchorNode(rootNode, identifiers, path);
122
// Restrict path to only those where the initializer has not changed
123
var pathToCheck = path.Skip(1).ToList();
124
var firstInitializerChangeNode = GetFirstInitializerChange(variableDeclarationStatement, pathToCheck, variableInitializer.Initializer);
125
if (firstInitializerChangeNode != null) {
126
// The node changing the initializer expression may not be on the path
127
// to the actual usages of the variable, so we need to merge the paths
128
// so we get the part of the paths that are common between them
129
var pathToChange = GetPath(rootNode, firstInitializerChangeNode);
130
var deepestCommonIndex = GetLowestCommonAncestorIndex(path, pathToChange);
131
anchorNode = pathToChange [deepestCommonIndex + 1];
132
path = pathToChange.Take(deepestCommonIndex).ToList();
135
// Restrict to locations outside of blacklisted node types
136
var firstBlackListedNode = path.Where(node => moveTargetBlacklist.Contains(node.GetType())).FirstOrDefault();
137
if (firstBlackListedNode != null) {
138
path = GetPath(rootNode, firstBlackListedNode.Parent);
139
anchorNode = firstBlackListedNode;
142
anchorNode = GetInsertionPoint(anchorNode);
144
if (anchorNode != null && anchorNode != rootNode && anchorNode.Parent != rootNode) {
145
AddIssue(variableDeclarationStatement, context.TranslateString("Variable could be moved to a nested scope"),
146
GetActions(variableDeclarationStatement, (Statement)anchorNode));
150
static bool IsBannedInsertionPoint(AstNode anchorNode)
152
var parent = anchorNode.Parent;
154
// Don't split 'else if ...' into else { if ... }
155
if (parent is IfElseStatement && anchorNode is IfElseStatement)
157
// Don't allow moving the declaration into the resource aquisition of a using statement
158
if (parent is UsingStatement)
160
// Don't allow moving things into arbitrary positions of for statements
161
if (parent is ForStatement && anchorNode.Role != Roles.EmbeddedStatement)
166
static AstNode GetInsertionPoint(AstNode node)
171
if (node is Statement && !IsBannedInsertionPoint(node))
178
AstNode GetInitialAnchorNode (BlockStatement rootNode, List<IdentifierExpression> identifiers, IList<AstNode> path)
180
if (identifiers.Count > 1) {
181
// Assume the first identifier is the first in the execution flow
182
// firstPath will always be longer than path since path is the
183
// combination of a least two (different) paths.
184
var firstPath = GetPath(rootNode, identifiers [0]);
185
if (firstPath [path.Count].Role == IfElseStatement.TrueRole) {
186
// IfElseStatement has a slightly weird structure; Don't
187
// consider the true role eligible for anchor node in this case
188
return firstPath [path.Count - 1];
190
return firstPath [path.Count];
192
// We only have one path, and a statement in itself cannot be an identifier
194
return path [path.Count - 1];
197
static IEnumerable<IdentifierExpression> GetIdentifiers(IEnumerable<AstNode> candidates, string name = null)
200
from node in candidates
201
let identifier = node as IdentifierExpression
202
where identifier != null && (name == null || identifier.Identifier == name)
206
AstNode GetFirstInitializerChange(AstNode variableDeclarationStatement, IList<AstNode> path, Expression initializer)
208
var identifiers = GetIdentifiers(initializer.DescendantsAndSelf).ToList();
209
var mayChangeInitializer = GetChecker (initializer, identifiers);
210
AstNode lastChange = null;
211
for (int i = path.Count - 1; i >= 0; i--) {
212
for (AstNode node = path[i].PrevSibling; node != null && node != variableDeclarationStatement; node = node.PrevSibling) {
213
// Special case for IfElseStatement: The AST nesting does not match the scope nesting, so
214
// don't handle branches here: The correct one has already been checked anyway.
215
// This also works to our advantage: No special checking is needed for the condition since
216
// it is a the same level in the tree as the false branch
217
if (node.Role == IfElseStatement.TrueRole || node.Role == IfElseStatement.FalseRole)
219
foreach (var expression in node.DescendantsAndSelf.Where(n => n is Expression).Cast<Expression>()) {
220
if (mayChangeInitializer(expression)) {
221
lastChange = expression;
229
Func<Expression, bool> GetChecker(Expression expression, IList<IdentifierExpression> identifiers)
231
// TODO: This only works for simple cases.
232
IList<IMember> members;
233
IList<IVariable> locals;
234
var identifierResolveResults = identifiers.Select(identifier => context.Resolve(identifier)).ToList();
235
SplitResolveResults(identifierResolveResults, out members, out locals);
237
if (expression is InvocationExpression || expression is ObjectCreateExpression) {
239
if (node is InvocationExpression || node is ObjectCreateExpression)
240
// We don't know what these might do, so assume it will change the initializer
242
var binaryOperator = node as BinaryOperatorExpression;
243
if (binaryOperator != null) {
244
var resolveResult = context.Resolve(binaryOperator) as OperatorResolveResult;
245
if (resolveResult == null)
247
// Built-in operators are ok, user defined ones not so much
248
return resolveResult.UserDefinedOperatorMethod != null;
250
return IsConflictingAssignment(node, identifiers, members, locals);
252
} else if (expression is IdentifierExpression) {
253
var initializerDependsOnMembers = identifierResolveResults.Any(result => result is MemberResolveResult);
254
var initializerDependsOnReferenceType = identifierResolveResults.Any(result => result.Type.IsReferenceType == true);
256
if ((node is InvocationExpression || node is ObjectCreateExpression) &&
257
(initializerDependsOnMembers || initializerDependsOnReferenceType))
258
// Anything can happen...
260
var binaryOperator = node as BinaryOperatorExpression;
261
if (binaryOperator != null) {
262
var resolveResult = context.Resolve(binaryOperator) as OperatorResolveResult;
263
if (resolveResult == null)
265
return resolveResult.UserDefinedOperatorMethod != null;
267
return IsConflictingAssignment(node, identifiers, members, locals);
271
return node => false;
274
bool IsConflictingAssignment (Expression node, IList<IdentifierExpression> identifiers, IList<IMember> members, IList<IVariable> locals)
276
var assignmentExpression = node as AssignmentExpression;
277
if (assignmentExpression != null) {
278
IList<IMember> targetMembers;
279
IList<IVariable> targetLocals;
280
var identifierResolveResults = identifiers.Select(identifier => context.Resolve(identifier)).ToList();
281
SplitResolveResults(identifierResolveResults, out targetMembers, out targetLocals);
283
return members.Any(member => targetMembers.Contains(member)) ||
284
locals.Any(local => targetLocals.Contains(local));
289
static void SplitResolveResults(List<ResolveResult> identifierResolveResults, out IList<IMember> members, out IList<IVariable> locals)
291
members = new List<IMember>();
292
locals = new List<IVariable>();
293
foreach (var resolveResult in identifierResolveResults) {
294
var memberResolveResult = resolveResult as MemberResolveResult;
295
if (memberResolveResult != null) {
296
members.Add(memberResolveResult.Member);
298
var localResolveResult = resolveResult as LocalResolveResult;
299
if (localResolveResult != null) {
300
locals.Add(localResolveResult.Variable);
305
bool IsScopeContainer(AstNode node)
310
var blockStatement = node as BlockStatement;
311
if (blockStatement != null)
314
var statement = node as Statement;
315
if (statement == null)
318
var role = node.Role;
319
if (role == Roles.EmbeddedStatement ||
320
role == IfElseStatement.TrueRole ||
321
role == IfElseStatement.FalseRole) {
327
IEnumerable<CodeAction> GetActions(Statement oldStatement, Statement followingStatement)
329
yield return new CodeAction(context.TranslateString("Move to nested scope"), script => {
330
var parent = followingStatement.Parent;
331
if (parent is SwitchSection || parent is BlockStatement) {
332
script.InsertBefore(followingStatement, oldStatement.Clone());
334
var newBlockStatement = new BlockStatement {
336
oldStatement.Clone(),
337
followingStatement.Clone()
340
script.Replace(followingStatement, newBlockStatement);
341
script.FormatText(parent);
343
script.Remove(oldStatement);
347
AstNode GetDeepestCommonAncestor(AstNode assumedRoot, IEnumerable<AstNode> leaves)
349
var previousPath = GetPath(assumedRoot, leaves.First());
350
int lowestIndex = previousPath.Count - 1;
351
foreach (var leaf in leaves.Skip(1)) {
352
var currentPath = GetPath(assumedRoot, leaf);
353
lowestIndex = GetLowestCommonAncestorIndex(previousPath, currentPath, lowestIndex);
354
previousPath = currentPath;
356
return previousPath [lowestIndex];
359
int GetLowestCommonAncestorIndex(IList<AstNode> path1, IList<AstNode> path2, int maxIndex = int.MaxValue)
361
var max = Math.Min(Math.Min(path1.Count, path2.Count), maxIndex);
362
for (int i = 0; i <= max; i++) {
363
if (path1 [i] != path2 [i])
369
IList<AstNode> GetPath(AstNode from, AstNode to)
371
var reversePath = new List<AstNode>();
375
} while (to != from.Parent);
376
reversePath.Reverse();