Skip to content

Commit af0f60b

Browse files
authored
Merge pull request #2654 from BZngr/next
#1942 - AssignedByValParameter new QuickFix
2 parents 314a2d0 + d0ebc1f commit af0f60b

14 files changed

+1018
-143
lines changed

RetailCoder.VBE/Common/StringExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,13 @@ public static string Captialize(this string input)
1515
tokens[0] = CultureInfo.CurrentUICulture.TextInfo.ToTitleCase(tokens[0]);
1616
return string.Join(" ", tokens);
1717
}
18+
public static string CapitalizeFirstLetter(this string input)
19+
{
20+
if (input.Length == 0)
21+
{
22+
return string.Empty;
23+
}
24+
return input.Captialize().Substring(0, 1) + input.Substring(1);
25+
}
1826
}
1927
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
using Rubberduck.Inspections.Abstract;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Rubberduck.VBEditor;
6+
using Rubberduck.Inspections.Resources;
7+
using Rubberduck.Parsing.Grammar;
8+
using Rubberduck.Parsing.Symbols;
9+
using System.Windows.Forms;
10+
using Rubberduck.UI.Refactorings;
11+
using Rubberduck.Common;
12+
13+
namespace Rubberduck.Inspections.QuickFixes
14+
{
15+
public class AssignedByValParameterQuickFix : QuickFixBase
16+
{
17+
18+
private Declaration _target;
19+
private string _localCopyVariableName;
20+
private bool _forceUseOfSuggestedName;
21+
private string[] _originalCodeLines;
22+
23+
public AssignedByValParameterQuickFix(Declaration target, QualifiedSelection selection)
24+
: base(target.Context, selection, InspectionsUI.AssignedByValParameterQuickFix)
25+
{
26+
_target = target;
27+
_forceUseOfSuggestedName = false;
28+
_localCopyVariableName = string.Empty;
29+
30+
_originalCodeLines = GetMethodLines();
31+
}
32+
public override bool CanFixInModule { get { return false; } }
33+
public override bool CanFixInProject { get { return false; } }
34+
35+
//This function exists solely to support unit testing - by preventing the popup dialog
36+
public void TESTONLY_FixUsingAutoGeneratedName()
37+
{
38+
_forceUseOfSuggestedName = true;
39+
Fix();
40+
}
41+
42+
public override void Fix()
43+
{
44+
if (_forceUseOfSuggestedName)
45+
{
46+
_localCopyVariableName = AutoSuggestedName();
47+
IsCancelled = false;
48+
}
49+
else
50+
{
51+
GetLocalCopyVariableNameFromUser();
52+
}
53+
54+
55+
if (!IsCancelled)
56+
{
57+
modifyBlockToUseLocalCopyVariable();
58+
}
59+
}
60+
61+
private void GetLocalCopyVariableNameFromUser()
62+
{
63+
using (var view = new AssignedByValParameterQuickFixDialog(_originalCodeLines))
64+
{
65+
view.Target = _target;
66+
view.NewName = AutoSuggestedName();
67+
view.ShowDialog();
68+
69+
IsCancelled = view.DialogResult == DialogResult.Cancel;
70+
if (!IsCancelled) { _localCopyVariableName = view.NewName; }
71+
}
72+
}
73+
private void modifyBlockToUseLocalCopyVariable()
74+
{
75+
if(ProposedNameIsInUse()) { return; }
76+
77+
var module = Selection.QualifiedName.Component.CodeModule;
78+
int startLine = Selection.Selection.StartLine;
79+
80+
module.InsertLines(++startLine, buildLocalCopyDeclaration());
81+
module.InsertLines(++startLine, buildLocalCopyAssignment());
82+
var moduleLines = GetModuleLines();
83+
//moduleLines array index is zero-based
84+
string endOfScopeStatement = GetEndOfScopeStatementForDeclaration(moduleLines[Selection.Selection.StartLine - 1]);
85+
86+
bool IsInScope = true;
87+
int obIndex; //One-Based index for module object
88+
int zbIndex; //Zero-Based index for moduleLines array
89+
//starts with lines after the above inserts
90+
for (zbIndex = startLine ; IsInScope && zbIndex < module.CountOfLines; zbIndex++)
91+
{
92+
obIndex = zbIndex + 1;
93+
if (LineRequiresUpdate(moduleLines[zbIndex]))
94+
{
95+
string newStatement = moduleLines[zbIndex].Replace(_target.IdentifierName, _localCopyVariableName);
96+
module.ReplaceLine(obIndex, newStatement);
97+
}
98+
IsInScope = !moduleLines[zbIndex].Contains(endOfScopeStatement);
99+
}
100+
}
101+
private bool ProposedNameIsInUse()
102+
{
103+
return GetMethodLines().Any(c => c.Contains(Tokens.Dim + " " + _localCopyVariableName + " "));
104+
}
105+
private bool LineRequiresUpdate(string line)
106+
{
107+
return line.Contains(" " + _target.IdentifierName + " ")
108+
|| line.Contains(NameAsLeftHandSide())
109+
|| line.Contains(NameAsRightHandSide())
110+
|| line.Contains(NameAsObjectMethodOrAccessorCall())
111+
|| line.Contains(NameAsSubOrFunctionParam())
112+
|| line.Contains(NameAsSubOrFunctionParamFirst())
113+
|| line.Contains(NameAsSubOrFunctionParamLast());
114+
}
115+
116+
private string NameAsLeftHandSide() { return _target.IdentifierName + " "; }
117+
private string NameAsRightHandSide() { return " " + _target.IdentifierName; }
118+
private string NameAsObjectMethodOrAccessorCall() { return " " + _target.IdentifierName + "."; }
119+
private string NameAsSubOrFunctionParam() { return _target.IdentifierName + ","; }
120+
private string NameAsSubOrFunctionParamFirst() { return "(" + _target.IdentifierName; }
121+
private string NameAsSubOrFunctionParamLast() { return _target.IdentifierName + ")"; }
122+
123+
124+
private string buildLocalCopyDeclaration()
125+
{
126+
return Tokens.Dim + " " + _localCopyVariableName + " " + Tokens.As
127+
+ " " + _target.AsTypeName;
128+
}
129+
private string buildLocalCopyAssignment()
130+
{
131+
return (ValueTypes.Contains(_target.AsTypeName) ? string.Empty : Tokens.Set + " ")
132+
+ _localCopyVariableName + " = " + _target.IdentifierName; ;
133+
}
134+
private string[] GetModuleLines()
135+
{
136+
var module = Selection.QualifiedName.Component.CodeModule;
137+
var lines = module.GetLines(1, module.CountOfLines);
138+
string[] newLine = new string[] { "\r\n" };
139+
return lines.Split(newLine, StringSplitOptions.None);
140+
}
141+
private string[] GetMethodLines()
142+
{
143+
int zbIndex; //zero-based index
144+
zbIndex = Selection.Selection.StartLine - 1;
145+
string[] allLines = GetModuleLines();
146+
147+
string endStatement = GetEndOfScopeStatementForDeclaration(allLines[zbIndex]);
148+
149+
bool IsInScope = true;
150+
var codeBlockLines = new List<string>();
151+
for ( ; IsInScope && zbIndex < allLines.Count(); zbIndex++)
152+
{
153+
codeBlockLines.Add(allLines[zbIndex]);
154+
IsInScope = !allLines[zbIndex].Contains(endStatement);
155+
}
156+
return codeBlockLines.ToArray();
157+
}
158+
private string GetEndOfScopeStatementForDeclaration(string declaration)
159+
{
160+
return declaration.Contains("Sub ") ? "End Sub" : "End Function";
161+
}
162+
private static readonly IReadOnlyList<string> ValueTypes = new[]
163+
{
164+
Tokens.Boolean,
165+
Tokens.Byte,
166+
Tokens.Currency,
167+
Tokens.Date,
168+
Tokens.Decimal,
169+
Tokens.Double,
170+
Tokens.Integer,
171+
Tokens.Long,
172+
Tokens.LongLong,
173+
Tokens.Single,
174+
Tokens.String,
175+
Tokens.Variant
176+
};
177+
private string AutoSuggestedName()
178+
{
179+
return "local" + this._target.IdentifierName.CapitalizeFirstLetter();
180+
}
181+
}
182+
}

RetailCoder.VBE/Inspections/Resources/InspectionsUI.Designer.cs

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

RetailCoder.VBE/Inspections/Resources/InspectionsUI.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,4 +647,7 @@ If the parameter can be null, ignore this inspection result; passing a null valu
647647
<data name="ApplicationWorksheetFunctionQuickFix" xml:space="preserve">
648648
<value>Use Application.WorksheetFunction explicitly.</value>
649649
</data>
650+
<data name="AssignedByValParameterQuickFix" xml:space="preserve">
651+
<value>Create and use a local copy of the parameter</value>
652+
</data>
650653
</root>

RetailCoder.VBE/Inspections/Results/AssignedByValParameterInspectionResult.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public override IEnumerable<QuickFixBase> QuickFixes
2727
{
2828
return _quickFixes ?? (_quickFixes = new QuickFixBase[]
2929
{
30+
new AssignedByValParameterQuickFix(Target, QualifiedSelection),
3031
new PassParameterByReferenceQuickFix(Target.Context, QualifiedSelection),
3132
new IgnoreOnceQuickFix(Context, QualifiedSelection, Inspection.AnnotationName)
3233
});

RetailCoder.VBE/Inspections/UseMeaningfulNameInspection.cs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,8 @@ public override IEnumerable<InspectionResultBase> GetInspectionResults()
6060

6161
private static bool IsBadIdentifier(string identifier)
6262
{
63-
return identifier.Length < 3 ||
64-
char.IsDigit(identifier.Last()) ||
65-
!HasVowels(identifier) ||
66-
NameIsASingleRepeatedLetter(identifier);
67-
}
68-
69-
private static bool HasVowels(string identifier)
70-
{
71-
const string vowels = "aeiouyàâäéèêëïîöôùûü";
72-
return identifier.Any(character => vowels.Any(vowel =>
73-
string.Compare(vowel.ToString(), character.ToString(), StringComparison.OrdinalIgnoreCase) == 0));
74-
}
75-
private static bool NameIsASingleRepeatedLetter(string identifierName)
76-
{
77-
string firstLetter = identifierName.First().ToString();
78-
return identifierName.All(a => string.Compare(a.ToString(), firstLetter,
79-
StringComparison.OrdinalIgnoreCase) == 0);
63+
var validator = new VariableNameValidator(identifier);
64+
return !validator.IsMeaningfulName();
8065
}
8166
}
8267
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using Rubberduck.Parsing.Grammar;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace Rubberduck.Inspections
9+
{
10+
public class VariableNameValidator
11+
{
12+
public VariableNameValidator() { }
13+
public VariableNameValidator(string identifier) { _identifier = identifier; }
14+
15+
16+
private const string _ALL_VOWELS = "aeiouyàâäéèêëïîöôùûü";
17+
private const int _MIN_VARIABLE_NAME_LENGTH = 3;
18+
19+
/**** Meaningful Name Characteristics ************/
20+
public bool HasVowels { get { return hasVowels(); } }
21+
public bool HasConsonants { get { return hasConsonants(); } }
22+
public bool IsSingleRepeatedLetter { get { return nameIsASingleRepeatedLetter(); } }
23+
public bool IsTooShort { get { return _identifier.Length < _MIN_VARIABLE_NAME_LENGTH; } }
24+
public bool EndsWithNumber { get { return endsWithNumber(); } }
25+
26+
/**** Invalid Name Characteristics ************/
27+
public bool StartsWithNumber { get { return FirstLetterIsDigit(); } }
28+
public bool IsReservedName { get { return isReservedName(); } }
29+
public bool ContainsSpecialCharacters { get { return UsesSpecialCharacters(); } }
30+
31+
private string _identifier;
32+
public string Identifier
33+
{
34+
get { return _identifier; }
35+
set { _identifier = value; }
36+
}
37+
public bool IsValidName()
38+
{
39+
return !StartsWithNumber
40+
&& !IsReservedName
41+
&& !ContainsSpecialCharacters;
42+
}
43+
public bool IsMeaningfulName()
44+
{
45+
return HasVowels
46+
&& HasConsonants
47+
&& !IsSingleRepeatedLetter
48+
&& !IsTooShort
49+
&& !EndsWithNumber;
50+
}
51+
private bool IsEmpty()
52+
{
53+
return _identifier.Equals(string.Empty);
54+
}
55+
private bool FirstLetterIsDigit()
56+
{
57+
return !char.IsLetter(_identifier.FirstOrDefault());
58+
}
59+
private bool isReservedName()
60+
{
61+
var tokenValues = typeof(Tokens).GetFields().Select(item => item.GetValue(null)).Cast<string>().Select(item => item);
62+
return tokenValues.Contains(_identifier, StringComparer.InvariantCultureIgnoreCase);
63+
}
64+
private bool UsesSpecialCharacters()
65+
{
66+
return _identifier.Any(c => !char.IsLetterOrDigit(c) && c != '_');
67+
}
68+
69+
private bool hasVowels()
70+
{
71+
return _identifier.Any(character => _ALL_VOWELS.Any(vowel =>
72+
string.Compare(vowel.ToString(), character.ToString(), StringComparison.OrdinalIgnoreCase) == 0));
73+
}
74+
private bool hasConsonants()
75+
{
76+
return !_identifier.All(character => _ALL_VOWELS.Any(vowel =>
77+
string.Compare(vowel.ToString(), character.ToString(), StringComparison.OrdinalIgnoreCase) == 0));
78+
}
79+
private bool nameIsASingleRepeatedLetter()
80+
{
81+
string firstLetter = _identifier.First().ToString();
82+
return _identifier.All(a => string.Compare(a.ToString(), firstLetter,
83+
StringComparison.OrdinalIgnoreCase) == 0);
84+
}
85+
private bool endsWithNumber()
86+
{
87+
return char.IsDigit(_identifier.Last());
88+
}
89+
}
90+
}

RetailCoder.VBE/Rubberduck.csproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@
380380
<Compile Include="Inspections\MemberNotOnInterfaceInspection.cs" />
381381
<Compile Include="Inspections\QuickFixes\AddIdentifierToWhiteListQuickFix.cs" />
382382
<Compile Include="Inspections\QuickFixes\ApplicationWorksheetFunctionQuickFix.cs" />
383+
<Compile Include="Inspections\QuickFixes\AssignedByValParameterQuickFix.cs" />
383384
<Compile Include="Inspections\Resources\InspectionsUI.Designer.cs">
384385
<AutoGen>True</AutoGen>
385386
<DesignTime>True</DesignTime>
@@ -442,6 +443,7 @@
442443
<Compile Include="Inspections\QuickFixes\SpecifyExplicitPublicModifierQuickFix.cs" />
443444
<Compile Include="Inspections\QuickFixes\WriteOnlyPropertyQuickFix.cs" />
444445
<Compile Include="Inspections\Results\WriteOnlyPropertyInspectionResult.cs" />
446+
<Compile Include="Inspections\VariableNameValidator.cs" />
445447
<Compile Include="Navigation\CodeExplorer\ICodeExplorerDeclarationViewModel.cs" />
446448
<Compile Include="Navigation\Folders\FolderHelper.cs" />
447449
<Compile Include="Refactorings\ExtractMethod\ExtractedMethod.cs" />
@@ -754,6 +756,12 @@
754756
<Compile Include="UI\IOpenFileDialog.cs" />
755757
<Compile Include="UI\ISaveFileDialog.cs" />
756758
<Compile Include="UI\ParserErrors\ParseErrorListItem.cs" />
759+
<Compile Include="UI\Refactorings\AssignedByValParameterQuickFixDialog.cs">
760+
<SubType>Form</SubType>
761+
</Compile>
762+
<Compile Include="UI\Refactorings\AssignedByValParameterQuickFixDialog.Designer.cs">
763+
<DependentUpon>AssignedByValParameterQuickFixDialog.cs</DependentUpon>
764+
</Compile>
757765
<Compile Include="UI\Refactorings\EncapsulateFieldDialog.cs">
758766
<SubType>Form</SubType>
759767
</Compile>
@@ -1098,6 +1106,9 @@
10981106
<EmbeddedResource Include="UI\FindSymbol\FindSymbolDialog.resx">
10991107
<DependentUpon>FindSymbolDialog.cs</DependentUpon>
11001108
</EmbeddedResource>
1109+
<EmbeddedResource Include="UI\Refactorings\AssignedByValParameterQuickFixDialog.resx">
1110+
<DependentUpon>AssignedByValParameterQuickFixDialog.cs</DependentUpon>
1111+
</EmbeddedResource>
11011112
<EmbeddedResource Include="UI\Refactorings\EncapsulateFieldDialog.resx">
11021113
<DependentUpon>EncapsulateFieldDialog.cs</DependentUpon>
11031114
</EmbeddedResource>

0 commit comments

Comments
 (0)