Skip to content

Commit d008172

Browse files
authored
Merge pull request #4371 from BZngr/4349_RenameClash
Enhances refactor/rename name scope clash validation
2 parents 89f2cb8 + 2f07e94 commit d008172

File tree

10 files changed

+463
-430
lines changed

10 files changed

+463
-430
lines changed

Rubberduck.CodeAnalysis/QuickFixes/AssignedByValParameterMakeLocalCopyQuickFix.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Rubberduck.Parsing.Symbols;
66
using Rubberduck.UI.Refactorings;
77
using Rubberduck.Common;
8-
using System.Collections.Generic;
98
using System.Windows.Forms;
109
using Antlr4.Runtime;
1110
using Rubberduck.Inspections.Abstract;
@@ -21,6 +20,7 @@ public sealed class AssignedByValParameterMakeLocalCopyQuickFix : QuickFixBase
2120
{
2221
private readonly IAssignedByValParameterQuickFixDialogFactory _dialogFactory;
2322
private readonly RubberduckParserState _parserState;
23+
private Declaration _quickFixTarget;
2424

2525
public AssignedByValParameterMakeLocalCopyQuickFix(RubberduckParserState state, IAssignedByValParameterQuickFixDialogFactory dialogFactory)
2626
: base(typeof(AssignedByValParameterInspection))
@@ -34,9 +34,9 @@ public override void Fix(IInspectionResult result)
3434
Debug.Assert(result.Target.Context.Parent is VBAParser.ArgListContext);
3535
Debug.Assert(null != ((ParserRuleContext)result.Target.Context.Parent.Parent).GetChild<VBAParser.EndOfStatementContext>());
3636

37-
var forbiddenNames = _parserState.DeclarationFinder.GetDeclarationsWithIdentifiersToAvoid(result.Target).Select(n => n.IdentifierName);
37+
_quickFixTarget = result.Target;
3838

39-
var localIdentifier = PromptForLocalVariableName(result.Target, forbiddenNames.ToList());
39+
var localIdentifier = PromptForLocalVariableName(result.Target);
4040
if (string.IsNullOrEmpty(localIdentifier))
4141
{
4242
return;
@@ -53,16 +53,16 @@ public override void Fix(IInspectionResult result)
5353
public override bool CanFixInModule => false;
5454
public override bool CanFixInProject => false;
5555

56-
private string PromptForLocalVariableName(Declaration target, List<string> forbiddenNames)
56+
private string PromptForLocalVariableName(Declaration target)
5757
{
5858
IAssignedByValParameterQuickFixDialog view = null;
5959
try
6060
{
61-
view = _dialogFactory.Create(target.IdentifierName, target.DeclarationType.ToString(), forbiddenNames);
62-
view.NewName = GetDefaultLocalIdentifier(target, forbiddenNames);
61+
view = _dialogFactory.Create(target.IdentifierName, target.DeclarationType.ToString(), IsNameCollision);
62+
view.NewName = GetDefaultLocalIdentifier(target);
6363
view.ShowDialog();
6464

65-
if (view.DialogResult == DialogResult.Cancel || !IsValidVariableName(view.NewName, forbiddenNames))
65+
if (view.DialogResult == DialogResult.Cancel || !IsValidVariableName(view.NewName))
6666
{
6767
return string.Empty;
6868
}
@@ -75,29 +75,32 @@ private string PromptForLocalVariableName(Declaration target, List<string> forbi
7575
}
7676
}
7777

78-
private string GetDefaultLocalIdentifier(Declaration target, List<string> forbiddenNames)
78+
private bool IsNameCollision(string newName)
79+
=> _parserState.DeclarationFinder.FindNewDeclarationNameConflicts(newName, _quickFixTarget).Any();
80+
81+
private string GetDefaultLocalIdentifier(Declaration target)
7982
{
8083
var newName = $"local{target.IdentifierName.CapitalizeFirstLetter()}";
81-
if (IsValidVariableName(newName, forbiddenNames))
84+
if (IsValidVariableName(newName))
8285
{
8386
return newName;
8487
}
8588

8689
for ( var attempt = 2; attempt < 10; attempt++)
8790
{
8891
var result = newName + attempt;
89-
if (IsValidVariableName(result, forbiddenNames))
92+
if (IsValidVariableName(result))
9093
{
9194
return result;
9295
}
9396
}
9497
return newName;
9598
}
9699

97-
private bool IsValidVariableName(string variableName, IEnumerable<string> forbiddenNames)
100+
private bool IsValidVariableName(string variableName)
98101
{
99102
return VariableNameValidator.IsValidName(variableName)
100-
&& !forbiddenNames.Any(name => name.Equals(variableName, StringComparison.InvariantCultureIgnoreCase));
103+
&& !IsNameCollision(variableName);
101104
}
102105

103106
private void ReplaceAssignedByValParameterReferences(IModuleRewriter rewriter, Declaration target, string localIdentifier)

Rubberduck.Core/UI/Refactorings/AssignedByValParameterQuickFixDialog.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Windows.Forms;
4-
using System.Linq;
53
using Rubberduck.Common;
64
using Rubberduck.Resources;
75

86
namespace Rubberduck.UI.Refactorings
97
{
108
public partial class AssignedByValParameterQuickFixDialog : Form, IAssignedByValParameterQuickFixDialog
119
{
12-
private readonly IEnumerable<string> _forbiddenNames;
10+
private readonly Func<string, bool> _isConflictingName;
1311

14-
public AssignedByValParameterQuickFixDialog(string identifier, string identifierType, IEnumerable<string> forbiddenNames)
12+
public AssignedByValParameterQuickFixDialog(string identifier, string identifierType, Func<string, bool> nameCollisionChecker)
1513
{
1614
InitializeComponent();
1715
InitializeCaptions(identifier, identifierType);
18-
_forbiddenNames = forbiddenNames;
16+
_isConflictingName = nameCollisionChecker;
1917
}
2018

2119
private void InitializeCaptions(string identifierName, string targetDeclarationType)
@@ -54,7 +52,7 @@ private string GetVariableNameFeedback()
5452
{
5553
return string.Empty;
5654
}
57-
if (_forbiddenNames.Any(name => name.Equals(NewName, StringComparison.OrdinalIgnoreCase)))
55+
if (_isConflictingName(NewName))
5856
{
5957
return string.Format(RubberduckUI.AssignedByValDialog_NewNameAlreadyUsedFormat, NewName);
6058
}
@@ -79,7 +77,7 @@ private string GetVariableNameFeedback()
7977

8078
private void SetControlsProperties()
8179
{
82-
var isValid = VariableNameValidator.IsValidName(NewName) && !_forbiddenNames.Any(name => name.Equals(NewName, StringComparison.OrdinalIgnoreCase));
80+
var isValid = VariableNameValidator.IsValidName(NewName);
8381
OkButton.Visible = isValid;
8482
OkButton.Enabled = isValid;
8583
InvalidNameValidationIcon.Visible = !isValid;
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
using Rubberduck.Parsing.Symbols;
2-
using System.Collections.Generic;
1+
using System;
32

43
namespace Rubberduck.UI.Refactorings
54
{
65
public interface IAssignedByValParameterQuickFixDialogFactory
76
{
8-
IAssignedByValParameterQuickFixDialog Create(string identifier, string identifierType, IEnumerable<string> forbiddenNames);
7+
IAssignedByValParameterQuickFixDialog Create(string identifier, string identifierType, Func<string, bool> nameCollisionChecker);
98
void Release(IAssignedByValParameterQuickFixDialog dialog);
109
}
1110
}

Rubberduck.Parsing/VBA/DeclarationCaching/DeclarationFinder.cs

Lines changed: 51 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,79 +1015,78 @@ private List<Declaration> FindAllEventHandlers()
10151015
return handlers.ToList();
10161016
}
10171017

1018-
public IEnumerable<Declaration> GetAccessibleUserDeclarations(Declaration target)
1018+
/// <summary>
1019+
/// Finds declarations that would be in conflict with the target declaration if renamed.
1020+
/// </summary>
1021+
/// <returns>Zero or more declarations that would be in conflict if the target declaration is renamed.</returns>
1022+
public IEnumerable<Declaration> FindNewDeclarationNameConflicts(string newName, Declaration renameTarget)
10191023
{
1020-
if (target == null)
1024+
if (newName.Equals(renameTarget.IdentifierName))
10211025
{
10221026
return Enumerable.Empty<Declaration>();
10231027
}
10241028

1025-
return _userDeclarationsByType.AllValues()
1026-
.Where(callee => AccessibilityCheck.IsAccessible(
1027-
Declaration.GetProjectParent(target),
1028-
Declaration.GetModuleParent(target),
1029-
target.ParentDeclaration,
1030-
callee));
1031-
}
1032-
1033-
public IEnumerable<Declaration> GetDeclarationsWithIdentifiersToAvoid(Declaration target)
1034-
{
1035-
if (target == null)
1029+
var identifierMatches = MatchName(newName);
1030+
if (!identifierMatches.Any())
10361031
{
10371032
return Enumerable.Empty<Declaration>();
10381033
}
10391034

1040-
List<Declaration> declarationsToAvoid = GetNameCollisionDeclarations(target).ToList();
1041-
1042-
declarationsToAvoid.AddRange(GetNameCollisionDeclarations(target.References));
1043-
1044-
return declarationsToAvoid.Distinct();
1045-
}
1046-
1047-
private IEnumerable<Declaration> GetNameCollisionDeclarations(Declaration declaration)
1048-
{
1049-
if (declaration == null)
1035+
if (IsEnumOrUDTMemberDeclaration(renameTarget))
10501036
{
1051-
return Enumerable.Empty<Declaration>();
1037+
var memberMatches = identifierMatches.Where(idm =>
1038+
IsEnumOrUDTMemberDeclaration(idm) && idm.ParentDeclaration == renameTarget.ParentDeclaration);
1039+
if (memberMatches.Any())
1040+
{
1041+
return memberMatches;
1042+
}
10521043
}
10531044

1054-
//Filter accessible declarations to those that would result in name collisions or hiding
1055-
var declarationsToAvoid = GetAccessibleUserDeclarations(declaration).Where(candidate =>
1056-
(IsAccessibleInOtherProcedureModule(candidate,declaration)
1057-
|| candidate.DeclarationType == DeclarationType.Project
1058-
|| candidate.DeclarationType.HasFlag(DeclarationType.Module)
1059-
|| IsDeclarationInSameProcedureScope(candidate, declaration)
1060-
)).ToHashSet();
1061-
1062-
//Add local variables when the target is a method or property
1063-
if(IsSubroutineOrProperty(declaration))
1045+
identifierMatches = identifierMatches.Where(nc => !IsEnumOrUDTMemberDeclaration(nc));
1046+
var referenceConflicts = identifierMatches.Where(idm =>
1047+
renameTarget.References
1048+
.Any(renameTargetRef => renameTargetRef.ParentScoping == idm.ParentDeclaration
1049+
|| renameTarget.ParentDeclaration.DeclarationType != DeclarationType.ClassModule
1050+
&& idm == renameTargetRef.ParentScoping
1051+
&& !UsesScopeResolution(renameTargetRef.Context.Parent)
1052+
|| idm.References
1053+
.Any(idmRef => idmRef.ParentScoping == renameTargetRef.ParentScoping
1054+
&& !UsesScopeResolution(renameTargetRef.Context.Parent)))
1055+
|| idm.DeclarationType.HasFlag(DeclarationType.Variable)
1056+
&& idm.ParentDeclaration.DeclarationType.HasFlag(DeclarationType.Module)
1057+
&& renameTarget.References.Any(renameTargetRef => renameTargetRef.QualifiedModuleName == idm.ParentDeclaration.QualifiedModuleName));
1058+
1059+
if (referenceConflicts.Any())
10641060
{
1065-
var localVariableDeclarations = _declarations.AllValues()
1066-
.Where(dec => ReferenceEquals(declaration, dec.ParentDeclaration));
1067-
declarationsToAvoid.UnionWith(localVariableDeclarations);
1061+
return referenceConflicts;
10681062
}
10691063

1070-
return declarationsToAvoid;
1064+
var renameTargetModule = Declaration.GetModuleParent(renameTarget);
1065+
var declarationConflicts = identifierMatches.Where(idm =>
1066+
renameTarget == idm.ParentDeclaration
1067+
|| AccessibilityCheck.IsAccessible(
1068+
Declaration.GetProjectParent(renameTarget),
1069+
renameTargetModule,
1070+
renameTarget.ParentDeclaration,
1071+
idm)
1072+
&& IsConflictingMember(renameTarget, renameTargetModule, idm));
1073+
1074+
return declarationConflicts;
10711075
}
10721076

1073-
private IEnumerable<Declaration> GetNameCollisionDeclarations(IEnumerable<IdentifierReference> references)
1077+
private bool IsEnumOrUDTMemberDeclaration(Declaration candidate)
10741078
{
1075-
var declarationsToAvoid = new HashSet<Declaration>();
1076-
foreach (var reference in references)
1077-
{
1078-
if (!UsesScopeResolution(reference.Context.Parent))
1079-
{
1080-
declarationsToAvoid.UnionWith(GetNameCollisionDeclarations(reference.ParentNonScoping));
1081-
}
1082-
}
1083-
return declarationsToAvoid;
1079+
return candidate.DeclarationType == DeclarationType.EnumerationMember
1080+
|| candidate.DeclarationType == DeclarationType.UserDefinedTypeMember;
10841081
}
10851082

1086-
private bool IsAccessibleInOtherProcedureModule(Declaration candidate, Declaration declaration)
1083+
private bool IsConflictingMember(Declaration renameTarget, Declaration renameTargetModule, Declaration candidate)
10871084
{
1088-
return IsInProceduralModule(declaration)
1089-
&& IsInProceduralModule(candidate)
1090-
&& candidate.Accessibility != Accessibility.Private;
1085+
var candidateModule = Declaration.GetModuleParent(candidate);
1086+
return renameTargetModule == candidateModule
1087+
|| renameTargetModule.DeclarationType.HasFlag(DeclarationType.ProceduralModule)
1088+
&& candidate.Accessibility != Accessibility.Private
1089+
&& candidateModule.DeclarationType.HasFlag(DeclarationType.ProceduralModule);
10911090
}
10921091

10931092
private bool UsesScopeResolution(RuleContext ruleContext)
@@ -1096,25 +1095,6 @@ private bool UsesScopeResolution(RuleContext ruleContext)
10961095
|| (ruleContext is VBAParser.MemberAccessExprContext);
10971096
}
10981097

1099-
private bool IsInProceduralModule(Declaration candidateDeclaration)
1100-
{
1101-
var candidateModuleDeclaration = Declaration.GetModuleParent(candidateDeclaration);
1102-
1103-
return candidateModuleDeclaration?.DeclarationType == DeclarationType.ProceduralModule;
1104-
}
1105-
1106-
private bool IsDeclarationInSameProcedureScope(Declaration candidateDeclaration, Declaration scopingDeclaration)
1107-
{
1108-
return candidateDeclaration.ParentScope == scopingDeclaration.ParentScope;
1109-
}
1110-
1111-
private static bool IsSubroutineOrProperty(Declaration declaration)
1112-
{
1113-
return declaration.DeclarationType.HasFlag(DeclarationType.Property)
1114-
|| declaration.DeclarationType == DeclarationType.Function
1115-
|| declaration.DeclarationType == DeclarationType.Procedure;
1116-
}
1117-
11181098
/// <summary>
11191099
/// Creates a dictionary of identifier references, keyed by module.
11201100
/// </summary>

Rubberduck.Refactorings/Rename/RenameRefactoring.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,11 @@ private bool TrySetNewName(IRenamePresenter presenter)
245245
var result = presenter.Show(_model.Target);
246246
if (result == null) { return false; }
247247

248-
var conflictDeclarations = _state.DeclarationFinder.GetDeclarationsWithIdentifiersToAvoid(_model.Target)
249-
.Where(d => d.IdentifierName.Equals(_model.NewName, StringComparison.InvariantCultureIgnoreCase)).ToList();
248+
var conflicts = _state.DeclarationFinder.FindNewDeclarationNameConflicts(_model.NewName, _model.Target);
250249

251-
if (conflictDeclarations.Any())
250+
if (conflicts.Any())
252251
{
253-
var message = string.Format(RubberduckUI.RenameDialog_ConflictingNames, _model.NewName,
254-
conflictDeclarations.First().IdentifierName);
252+
var message = string.Format(RubberduckUI.RenameDialog_ConflictingNames, _model.NewName, _model.Target.IdentifierName);
255253

256254
return _messageBox?.ConfirmYesNo(message, RubberduckUI.RenameDialog_Caption) ?? false;
257255
}

Rubberduck.Resources/RubberduckUI.Designer.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rubberduck.Resources/RubberduckUI.resx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,9 @@ Warning: All customized settings will be lost. Your old file will be saved in '
484484
<value>New</value>
485485
</data>
486486
<data name="RenameDialog_ConflictingNames" xml:space="preserve">
487-
<value>Renaming to '{0}' clashes with '{1}' in the same scope.
488-
Are you sure you want to proceed with this rename?</value>
487+
<value>'{0}' conflicts with an existing name. Renaming '{1}' to '{0}' may result in uncompilable code or a change in logic.
488+
Do you want to proceed with this rename?</value>
489+
<comment>0: NewName 1: CurrentName</comment>
489490
</data>
490491
<data name="RenameDialog_AmbiguousSelection" xml:space="preserve">
491492
<value>Please ensure that exactly 1 control is selected before renaming.</value>

RubberduckTests/QuickFixes/AssignedByValParameterMakeLocalCopyQuickFixTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using NUnit.Framework;
2-
using System.Collections.Generic;
32
using System.Linq;
43
using System.Threading;
54
using Rubberduck.Inspections.QuickFixes;
@@ -9,6 +8,7 @@
98
using Rubberduck.UI.Refactorings;
109
using System.Windows.Forms;
1110
using Rubberduck.Inspections.Concrete;
11+
using System;
1212

1313
namespace RubberduckTests.QuickFixes
1414
{
@@ -392,7 +392,7 @@ private Mock<IAssignedByValParameterQuickFixDialogFactory> BuildMockDialogFactor
392392
mockDialog.SetupGet(m => m.DialogResult).Returns(() => DialogResult.OK);
393393

394394
var mockDialogFactory = new Mock<IAssignedByValParameterQuickFixDialogFactory>();
395-
mockDialogFactory.Setup(f => f.Create(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>())).Returns(mockDialog.Object);
395+
mockDialogFactory.Setup(f => f.Create(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Func<string,bool>>())).Returns(mockDialog.Object);
396396

397397
return mockDialogFactory;
398398
}

0 commit comments

Comments
 (0)