-
Notifications
You must be signed in to change notification settings - Fork 382
/
AlignAssignmentStatement.cs
333 lines (298 loc) · 12.6 KB
/
AlignAssignmentStatement.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
#if !CORECLR
using System.ComponentModel.Composition;
#endif
using System.Globalization;
using System.Linq;
using System.Management.Automation.Language;
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
/// <summary>
/// AlignAssignmentStatement: Checks if consecutive assignment statements are aligned.
/// </summary>
#if !CORECLR
[Export(typeof(IScriptRule))]
#endif
public class AlignAssignmentStatement : ConfigurableRule
{
// We keep this switch even though the rule has only one switch (this) as of now, because we want
// to let the rule be expandable in the future to allow formatting assignments even
// in variable assignments. But for now we will stick to only one option.
/// <summary>
/// Check if key value pairs in a hashtable are aligned or not.
/// </summary>
/// <returns></returns>
[ConfigurableRuleProperty(defaultValue: true)]
public bool CheckHashtable { get; set; }
private readonly char whitespaceChar = ' ';
private List<Func<TokenOperations, IEnumerable<DiagnosticRecord>>> violationFinders
= new List<Func<TokenOperations, IEnumerable<DiagnosticRecord>>>();
/// <summary>
/// Sets the configurable properties of this rule.
/// </summary>
/// <param name="paramValueMap">A dictionary that maps parameter name to it value. Must be non-null</param>
public override void ConfigureRule(IDictionary<string, object> paramValueMap)
{
base.ConfigureRule(paramValueMap);
if (CheckHashtable)
{
violationFinders.Add(FindHashtableViolations);
}
}
/// <summary>
/// Analyzes the given ast to find if consecutive assignment statements are aligned.
/// </summary>
/// <param name="ast">AST to be analyzed. This should be non-null</param>
/// <param name="fileName">Name of file that corresponds to the input AST.</param>
/// <returns>A an enumerable type containing the violations</returns>
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (ast == null)
{
throw new ArgumentNullException("ast");
}
// only handles one line assignments
// if the rule encounters assignment statements that are multi-line, the rule will ignore that block
var tokenOps = new TokenOperations(Helper.Instance.Tokens, ast);
foreach (var violationFinder in violationFinders)
{
foreach (var diagnosticRecord in violationFinder(tokenOps))
{
yield return diagnosticRecord;
}
}
}
/// <summary>
/// Retrieves the common name of this rule.
/// </summary>
public override string GetCommonName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementCommonName);
}
/// <summary>
/// Retrieves the description of this rule.
/// </summary>
public override string GetDescription()
{
return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementDescription);
}
/// <summary>
/// Retrieves the name of this rule.
/// </summary>
public override string GetName()
{
return string.Format(
CultureInfo.CurrentCulture,
Strings.NameSpaceFormat,
GetSourceName(),
Strings.AlignAssignmentStatementName);
}
/// <summary>
/// Retrieves the severity of the rule: error, warning or information.
/// </summary>
public override RuleSeverity GetSeverity()
{
return RuleSeverity.Warning;
}
/// <summary>
/// Gets the severity of the returned diagnostic record: error, warning, or information.
/// </summary>
/// <returns></returns>
public DiagnosticSeverity GetDiagnosticSeverity()
{
return DiagnosticSeverity.Warning;
}
/// <summary>
/// Retrieves the name of the module/assembly the rule is from.
/// </summary>
public override string GetSourceName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
}
/// <summary>
/// Retrieves the type of the rule, Builtin, Managed or Module.
/// </summary>
public override SourceType GetSourceType()
{
return SourceType.Builtin;
}
private IEnumerable<DiagnosticRecord> FindHashtableViolations(TokenOperations tokenOps)
{
var hashtableAsts = tokenOps.Ast.FindAll(ast => ast is HashtableAst, true);
var groups = new List<List<Tuple<IScriptExtent, IScriptExtent>>>();
if (hashtableAsts != null)
{
foreach (var astItem in hashtableAsts)
{
groups.Add(GetExtents(tokenOps, (HashtableAst)astItem));
}
}
#if !PSV3
var configAsts = tokenOps.Ast.FindAll(ast => ast is ConfigurationDefinitionAst, true);
if (configAsts != null)
{
// There are probably parse errors caused by an "Undefined DSC resource"
// which prevents the parser from detecting the property value pairs as
// hashtable. Hence, this is a workaround to format configurations which
// have "Undefined DSC resource" parse errors.
// find all commandAsts of the form "prop" "=" "val" that have the same parent
// and format those pairs.
foreach (var configAst in configAsts)
{
groups.AddRange(GetCommandElementExtentGroups(configAst));
}
}
#endif
// it is probably much easier have a hashtable writer that formats the hashtable and writes it
// but it makes handling comments hard. So we need to use this approach.
// This is how the algorithm actually works:
// if each key value pair are on a separate line
// find all the assignment operators
// if all the assignment operators are aligned (check the column number of each assignment operator)
// skip
// else
// find the distance between the assignment operators and their corresponding LHS
// find the longest left expression
// make sure all the assignment operators are in the same column as that of the longest left hand.
foreach (var extentTuples in groups)
{
if (!HasPropertiesOnSeparateLines(extentTuples))
{
continue;
}
if (extentTuples == null
|| extentTuples.Count == 0
|| !extentTuples.All(t => t.Item1.StartLineNumber == t.Item2.EndLineNumber))
{
continue;
}
var expectedStartColumnNumber = extentTuples.Max(x => x.Item1.EndColumnNumber) + 1;
foreach (var extentTuple in extentTuples)
{
if (extentTuple.Item2.StartColumnNumber != expectedStartColumnNumber)
{
yield return new DiagnosticRecord(
GetError(),
extentTuple.Item2,
GetName(),
GetDiagnosticSeverity(),
extentTuple.Item1.File,
null,
GetHashtableCorrections(extentTuple, expectedStartColumnNumber).ToList());
}
}
}
}
private List<List<Tuple<IScriptExtent, IScriptExtent>>> GetCommandElementExtentGroups(Ast configAst)
{
var result = new List<List<Tuple<IScriptExtent, IScriptExtent>>>();
var commandAstGroups = GetCommandElementGroups(configAst);
foreach (var commandAstGroup in commandAstGroups)
{
var list = new List<Tuple<IScriptExtent, IScriptExtent>>();
foreach (var commandAst in commandAstGroup)
{
var elems = commandAst.CommandElements;
list.Add(new Tuple<IScriptExtent, IScriptExtent>(elems[0].Extent, elems[1].Extent));
}
result.Add(list);
}
return result;
}
private List<List<CommandAst>> GetCommandElementGroups(Ast configAst)
{
var result = new List<List<CommandAst>>();
var astsFound = configAst.FindAll(ast => IsPropertyValueCommandAst(ast), true);
if (astsFound == null)
{
return result;
}
var parentChildrenGroup = from ast in astsFound
select (CommandAst)ast into commandAst
group commandAst by commandAst.Parent.Parent; // parent is pipeline and pipeline's parent is namedblockast
foreach (var group in parentChildrenGroup)
{
result.Add(group.ToList());
}
return result;
}
private bool IsPropertyValueCommandAst(Ast ast)
{
var commandAst = ast as CommandAst;
return commandAst != null
&& commandAst.CommandElements.Count() == 3
&& commandAst.CommandElements[1].Extent.Text.Equals("=");
}
private IEnumerable<CorrectionExtent> GetHashtableCorrections(
Tuple<IScriptExtent, IScriptExtent> extentTuple,
int expectedStartColumnNumber)
{
var equalExtent = extentTuple.Item2;
var lhsExtent = extentTuple.Item1;
var columnDiff = expectedStartColumnNumber - equalExtent.StartColumnNumber;
yield return new CorrectionExtent(
lhsExtent.EndLineNumber,
equalExtent.StartLineNumber,
lhsExtent.EndColumnNumber,
equalExtent.StartColumnNumber,
new String(whitespaceChar, expectedStartColumnNumber - lhsExtent.EndColumnNumber),
GetError());
}
private string GetError()
{
return String.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementError);
}
private static List<Tuple<IScriptExtent, IScriptExtent>> GetExtents(
TokenOperations tokenOps,
HashtableAst hashtableAst)
{
var nodeTuples = new List<Tuple<IScriptExtent, IScriptExtent>>();
foreach (var kvp in hashtableAst.KeyValuePairs)
{
var keyStartOffset = kvp.Item1.Extent.StartOffset;
bool keyStartOffSetReached = false;
var keyTokenNode = tokenOps.GetTokenNodes(
token =>
{
if (keyStartOffSetReached)
{
return token.Kind == TokenKind.Equals;
}
if (token.Extent.StartOffset == keyStartOffset)
{
keyStartOffSetReached = true;
}
return false;
}).FirstOrDefault();
if (keyTokenNode == null || keyTokenNode.Value == null)
{
continue;
}
var assignmentToken = keyTokenNode.Value.Extent;
nodeTuples.Add(new Tuple<IScriptExtent, IScriptExtent>(
kvp.Item1.Extent, assignmentToken));
}
return nodeTuples;
}
private bool HasPropertiesOnSeparateLines(IEnumerable<Tuple<IScriptExtent, IScriptExtent>> tuples)
{
var lines = new HashSet<int>();
foreach (var kvp in tuples)
{
if (lines.Contains(kvp.Item1.StartLineNumber))
{
return false;
}
else
{
lines.Add(kvp.Item1.StartLineNumber);
}
}
return true;
}
}
}