Skip to content

Commit 30f3d0f

Browse files
author
Quoc Truong
committed
Merge pull request #278 from PowerShell/profile
Implement GlobalSuppression for ScriptAnalyzer
2 parents a98d768 + 844d47d commit 30f3d0f

7 files changed

+265
-13
lines changed

Engine/Commands/InvokeScriptAnalyzerCommand.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,19 @@ public SwitchParameter SuppressedOnly
123123
}
124124
private bool suppressedOnly;
125125

126-
#endregion Parameters
127-
128-
#region Private Members
129-
130-
Dictionary<string, List<string>> validationResults = new Dictionary<string, List<string>>();
126+
/// <summary>
127+
/// Returns path to the file that contains user profile for ScriptAnalyzer
128+
/// </summary>
129+
[Parameter(Mandatory = false)]
130+
[ValidateNotNull]
131+
public string Profile
132+
{
133+
get { return profile; }
134+
set { profile = value; }
135+
}
136+
private string profile;
131137

132-
#endregion
138+
#endregion Parameters
133139

134140
#region Overrides
135141

@@ -144,7 +150,8 @@ protected override void BeginProcessing()
144150
this.includeRule,
145151
this.excludeRule,
146152
this.severity,
147-
this.suppressedOnly);
153+
this.suppressedOnly,
154+
this.profile);
148155
}
149156

150157
/// <summary>

Engine/ScriptAnalyzer.cs

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ internal void Initialize<TCmdlet>(
9999
string[] includeRuleNames = null,
100100
string[] excludeRuleNames = null,
101101
string[] severity = null,
102-
bool suppressedOnly = false)
102+
bool suppressedOnly = false,
103+
string profile = null)
103104
where TCmdlet : PSCmdlet, IOutputWriter
104105
{
105106
if (cmdlet == null)
@@ -115,7 +116,8 @@ internal void Initialize<TCmdlet>(
115116
includeRuleNames,
116117
excludeRuleNames,
117118
severity,
118-
suppressedOnly);
119+
suppressedOnly,
120+
profile);
119121
}
120122

121123
/// <summary>
@@ -128,7 +130,8 @@ public void Initialize(
128130
string[] includeRuleNames = null,
129131
string[] excludeRuleNames = null,
130132
string[] severity = null,
131-
bool suppressedOnly = false)
133+
bool suppressedOnly = false,
134+
string profile = null)
132135
{
133136
if (runspace == null)
134137
{
@@ -143,7 +146,8 @@ public void Initialize(
143146
includeRuleNames,
144147
excludeRuleNames,
145148
severity,
146-
suppressedOnly);
149+
suppressedOnly,
150+
profile);
147151
}
148152

149153
private void Initialize(
@@ -154,7 +158,8 @@ private void Initialize(
154158
string[] includeRuleNames,
155159
string[] excludeRuleNames,
156160
string[] severity,
157-
bool suppressedOnly = false)
161+
bool suppressedOnly = false,
162+
string profile = null)
158163
{
159164
if (outputWriter == null)
160165
{
@@ -178,6 +183,145 @@ private void Initialize(
178183
this.includeRegexList = new List<Regex>();
179184
this.excludeRegexList = new List<Regex>();
180185

186+
if (!String.IsNullOrWhiteSpace(profile))
187+
{
188+
try
189+
{
190+
profile = path.GetResolvedPSPathFromPSPath(profile).First().Path;
191+
}
192+
catch
193+
{
194+
this.outputWriter.WriteError(new ErrorRecord(new FileNotFoundException(),
195+
string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, profile),
196+
ErrorCategory.InvalidArgument, this));
197+
}
198+
199+
if (File.Exists(profile))
200+
{
201+
Token[] parserTokens = null;
202+
ParseError[] parserErrors = null;
203+
Ast profileAst = Parser.ParseFile(profile, out parserTokens, out parserErrors);
204+
IEnumerable<Ast> hashTableAsts = profileAst.FindAll(item => item is HashtableAst, false);
205+
foreach (HashtableAst hashTableAst in hashTableAsts)
206+
{
207+
foreach (var kvp in hashTableAst.KeyValuePairs)
208+
{
209+
if (!(kvp.Item1 is StringConstantExpressionAst))
210+
{
211+
this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(),
212+
string.Format(CultureInfo.CurrentCulture, Strings.WrongKeyFormat, kvp.Item1.Extent.StartLineNumber, kvp.Item1.Extent.StartColumnNumber, profile),
213+
ErrorCategory.InvalidArgument, this));
214+
continue;
215+
}
216+
217+
// parse the item2 as array
218+
PipelineAst pipeAst = kvp.Item2 as PipelineAst;
219+
List<string> rhsList = new List<string>();
220+
if (pipeAst != null)
221+
{
222+
ExpressionAst pureExp = pipeAst.GetPureExpression();
223+
if (pureExp is StringConstantExpressionAst)
224+
{
225+
rhsList.Add((pureExp as StringConstantExpressionAst).Value);
226+
}
227+
else
228+
{
229+
ArrayLiteralAst arrayLitAst = pureExp as ArrayLiteralAst;
230+
if (arrayLitAst == null && pureExp is ArrayExpressionAst)
231+
{
232+
ArrayExpressionAst arrayExp = pureExp as ArrayExpressionAst;
233+
// Statements property is never null
234+
if (arrayExp.SubExpression != null)
235+
{
236+
StatementAst stateAst = arrayExp.SubExpression.Statements.First();
237+
if (stateAst != null && stateAst is PipelineAst)
238+
{
239+
CommandBaseAst cmdBaseAst = (stateAst as PipelineAst).PipelineElements.First();
240+
if (cmdBaseAst != null && cmdBaseAst is CommandExpressionAst)
241+
{
242+
CommandExpressionAst cmdExpAst = cmdBaseAst as CommandExpressionAst;
243+
if (cmdExpAst.Expression is StringConstantExpressionAst)
244+
{
245+
rhsList.Add((cmdExpAst.Expression as StringConstantExpressionAst).Value);
246+
}
247+
else
248+
{
249+
arrayLitAst = cmdExpAst.Expression as ArrayLiteralAst;
250+
}
251+
}
252+
}
253+
}
254+
}
255+
256+
if (arrayLitAst != null)
257+
{
258+
foreach (var element in arrayLitAst.Elements)
259+
{
260+
if (!(element is StringConstantExpressionAst))
261+
{
262+
this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(),
263+
string.Format(CultureInfo.CurrentCulture, Strings.WrongValueFormat, element.Extent.StartLineNumber, element.Extent.StartColumnNumber, profile),
264+
ErrorCategory.InvalidArgument, this));
265+
continue;
266+
}
267+
268+
rhsList.Add((element as StringConstantExpressionAst).Value);
269+
}
270+
}
271+
}
272+
}
273+
274+
if (rhsList.Count == 0)
275+
{
276+
this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(),
277+
string.Format(CultureInfo.CurrentCulture, Strings.WrongValueFormat, kvp.Item2.Extent.StartLineNumber, kvp.Item2.Extent.StartColumnNumber, profile),
278+
ErrorCategory.InvalidArgument, this));
279+
break;
280+
}
281+
282+
switch ((kvp.Item1 as StringConstantExpressionAst).Value.ToLower())
283+
{
284+
case "severity":
285+
if (this.severity == null)
286+
{
287+
this.severity = rhsList.ToArray();
288+
}
289+
else
290+
{
291+
this.severity = this.severity.Union(rhsList).ToArray();
292+
}
293+
break;
294+
case "includerules":
295+
if (this.includeRule == null)
296+
{
297+
this.includeRule = rhsList.ToArray();
298+
}
299+
else
300+
{
301+
this.includeRule = this.includeRule.Union(rhsList).ToArray();
302+
}
303+
break;
304+
case "excluderules":
305+
if (this.excludeRule == null)
306+
{
307+
this.excludeRule = rhsList.ToArray();
308+
}
309+
else
310+
{
311+
this.excludeRule = this.excludeRule.Union(rhsList).ToArray();
312+
}
313+
break;
314+
default:
315+
this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(),
316+
string.Format(CultureInfo.CurrentCulture, Strings.WrongKey, kvp.Item1.Extent.StartLineNumber, kvp.Item1.Extent.StartColumnNumber, profile),
317+
ErrorCategory.InvalidArgument, this));
318+
break;
319+
}
320+
}
321+
}
322+
}
323+
}
324+
181325
//Check wild card input for the Include/ExcludeRules and create regex match patterns
182326
if (this.includeRule != null)
183327
{

Engine/Strings.Designer.cs

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

Engine/Strings.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,13 @@
192192
<data name="RuleSuppressionIDError" xml:space="preserve">
193193
<value>Cannot find any DiagnosticRecord with the Rule Suppression ID {0}.</value>
194194
</data>
195+
<data name="WrongKey" xml:space="preserve">
196+
<value>{0} is not a valid key in the profile hashtable: line {0} column {1} in file {2}</value>
197+
</data>
198+
<data name="WrongKeyFormat" xml:space="preserve">
199+
<value>Key in the profile hashtable should be a string: line {0} column {1} in file {2}</value>
200+
</data>
201+
<data name="WrongValueFormat" xml:space="preserve">
202+
<value>Value in the profile hashtable should be a string or an array of strings: line {0} column {1} in file {2}</value>
203+
</data>
195204
</root>

Tests/Engine/GlobalSuppression.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
$a
2+
3+
gcm "blah"
4+
5+
$a = "//internal"
6+
7+
Get-Alias -ComputerName dfd
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Check if PSScriptAnalyzer is already loaded so we don't
2+
# overwrite a test version of Invoke-ScriptAnalyzer by
3+
# accident
4+
if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage)
5+
{
6+
Import-Module -Verbose PSScriptAnalyzer
7+
}
8+
9+
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
10+
$violations = Invoke-ScriptAnalyzer $directory\GlobalSuppression.ps1
11+
$suppression = Invoke-ScriptAnalyzer $directory\GlobalSuppression.ps1 -Profile $directory\Profile.ps1
12+
13+
Describe "GlobalSuppression" {
14+
Context "Exclude Rule" {
15+
It "Raises 1 violation for uninitialized variable and 1 for cmdlet alias" {
16+
$withoutProfile = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingCmdletAliases" -or $_.RuleName -eq "PSAvoidUninitializedVariable" }
17+
$withoutProfile.Count | Should Be 2
18+
}
19+
20+
It "Does not raise any violations for uninitialized variable and cmdlet alias with profile" {
21+
$withProfile = $suppression | Where-Object { $_.RuleName -eq "PSAvoidUsingCmdletAliases" -or $_.RuleName -eq "PSAvoidUninitializedVariable" }
22+
$withProfile.Count | Should be 0
23+
}
24+
}
25+
26+
Context "Include Rule" {
27+
It "Raises 1 violation for computername hard-coded" {
28+
$withoutProfile = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingComputerNameHardcoded" }
29+
$withoutProfile.Count | Should Be 1
30+
}
31+
32+
It "Does not raise any violations for computername hard-coded" {
33+
$withProfile = $suppression | Where-Object { $_.RuleName -eq "PSAvoidUsingComputerNameHardcoded" }
34+
$withProfile.Count | Should be 0
35+
}
36+
}
37+
38+
Context "Severity" {
39+
It "Raises 1 violation for internal url without profile" {
40+
$withoutProfile = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingInternalURLs" }
41+
$withoutProfile.Count | Should Be 1
42+
}
43+
44+
It "Does not raise any violations for internal urls with profile" {
45+
$withProfile = $suppression | Where-Object { $_.RuleName -eq "PSAvoidUsingInternalURLs" }
46+
$withProfile.Count | Should be 0
47+
}
48+
}
49+
}

Tests/Engine/Profile.ps1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@{
2+
Severity='Warning'
3+
IncludeRules=@('PSAvoidUsingCmdletAliases',
4+
'PSAvoidUsingPositionalParameters',
5+
'PSAvoidUsingInternalURLs'
6+
'PSAvoidUninitializedVariable')
7+
ExcludeRules=@('PSAvoidUsingCmdletAliases'
8+
'PSAvoidUninitializedVariable')
9+
}

0 commit comments

Comments
 (0)