Skip to content

Commit 3c19861

Browse files
committed
Added -AutoFix switch on Invoke-ScriptAnalyzer for the 'File' ParameterSet and implement it for PSAvoidUsingCmdletAliases.
It uses the information available in the DiagnosticRecord class to fix the rule.
1 parent 447eca6 commit 3c19861

File tree

4 files changed

+129
-10
lines changed

4 files changed

+129
-10
lines changed

Engine/Commands/InvokeScriptAnalyzerCommand.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
4242
public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter
4343
{
4444
#region Private variables
45-
List<string> processedPaths;
45+
private List<string> processedPaths;
4646
#endregion // Private variables
4747

4848
#region Parameters
@@ -117,6 +117,18 @@ public SwitchParameter IncludeDefaultRules
117117
}
118118
private bool includeDefaultRules;
119119

120+
/// <summary>
121+
/// Resolves rule violations automatically where possible.
122+
/// </summary>
123+
[Parameter(Mandatory = false, ParameterSetName = "File")]
124+
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
125+
public SwitchParameter AutoFix
126+
{
127+
get { return autoFix; }
128+
set { autoFix = value; }
129+
}
130+
private bool autoFix;
131+
120132
/// <summary>
121133
/// ExcludeRule: Array of names of rules to be disabled.
122134
/// </summary>
@@ -351,7 +363,7 @@ private void ProcessInput()
351363
{
352364
foreach (var p in processedPaths)
353365
{
354-
diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath(p, this.recurse);
366+
diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath(p, this.recurse, this.autoFix);
355367
WriteToOutput(diagnosticsList);
356368
}
357369
}

Engine/Generic/DiagnosticRecord.cs

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515
using System;
1616
using System.Collections.Generic;
17+
using System.IO;
18+
using System.Linq;
1719
using System.Management.Automation.Language;
20+
using System.Security;
1821

1922
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
2023
{
@@ -31,6 +34,7 @@ public class DiagnosticRecord
3134
private string scriptPath;
3235
private string ruleSuppressionId;
3336
private List<CorrectionExtent> suggestedCorrections;
37+
private bool canBeFixedAutomatically;
3438

3539
/// <summary>
3640
/// Represents a string from the rule about why this diagnostic was created.
@@ -73,7 +77,7 @@ public DiagnosticSeverity Severity
7377
/// </summary>
7478
public string ScriptName
7579
{
76-
get { return string.IsNullOrEmpty(scriptPath) ? string.Empty : System.IO.Path.GetFileName(scriptPath);}
80+
get { return string.IsNullOrEmpty(scriptPath) ? string.Empty : System.IO.Path.GetFileName(scriptPath); }
7781
}
7882

7983
/// <summary>
@@ -103,6 +107,14 @@ public IEnumerable<CorrectionExtent> SuggestedCorrections
103107
get { return suggestedCorrections; }
104108
}
105109

110+
/// <summary>
111+
/// Returns whether it can be corrected automatically using the <see cref="AutoFix"/> method or by using the '-Fix' switch of Invoke-Scriptanalyzer
112+
/// </summary>
113+
public bool CanBeFixedAutomatically
114+
{
115+
get { return canBeFixedAutomatically; }
116+
}
117+
106118
/// <summary>
107119
/// DiagnosticRecord: The constructor for DiagnosticRecord class.
108120
/// </summary>
@@ -120,20 +132,85 @@ public DiagnosticRecord()
120132
/// <param name="severity">The severity of this diagnostic</param>
121133
/// <param name="scriptPath">The full path of the script file being analyzed</param>
122134
/// <param name="suggestedCorrections">The correction suggested by the rule to replace the extent text</param>
123-
public DiagnosticRecord(string message, IScriptExtent extent, string ruleName, DiagnosticSeverity severity, string scriptPath, string ruleId = null, List<CorrectionExtent> suggestedCorrections = null)
135+
/// <param name="canBeFixedAutomatically">Enough information is present in this object for an automatic if a scriptPath is present</param>
136+
public DiagnosticRecord(string message, IScriptExtent extent, string ruleName, DiagnosticSeverity severity, string scriptPath, string ruleId = null, List<CorrectionExtent> suggestedCorrections = null, bool canBeFixedAutomatically = false)
124137
{
125-
Message = message;
138+
Message = message;
126139
RuleName = ruleName;
127-
Extent = extent;
140+
Extent = extent;
128141
Severity = severity;
129142
ScriptPath = scriptPath;
130143
RuleSuppressionID = ruleId;
131144
this.suggestedCorrections = suggestedCorrections;
145+
this.canBeFixedAutomatically = canBeFixedAutomatically;
132146
}
133147

148+
/// <summary>
149+
/// Uses the first element in the list SuggestedCorrections for the fix and replaces it with the Extentent.Text property
150+
/// Only supported for files at the moment.
151+
/// </summary>
152+
internal void AutoFix()
153+
{
154+
if (!string.IsNullOrEmpty(this.scriptPath))
155+
{
156+
var textToBeReplaced = this.Extent.Text;
157+
string textReplacement = this.SuggestedCorrections.FirstOrDefault().Text;
158+
159+
// Fix rule
160+
var scriptPath = this.ScriptPath;
161+
string[] originalLines = new string[] { };
162+
try
163+
{
164+
originalLines = File.ReadAllLines(scriptPath);
165+
}
166+
catch (Exception e) // because the file was already read before, we do not expect errors and therefore it is not worth catching specifically
167+
{
168+
Console.WriteLine($"Error reading file {scriptPath}. Exception: {e.Message}");
169+
}
170+
var lineNumber = this.SuggestedCorrections.FirstOrDefault().StartLineNumber;
171+
originalLines[lineNumber - 1] = originalLines[lineNumber - 1].Remove(this.Extent.StartColumnNumber - 1, textToBeReplaced.Length);
172+
originalLines[lineNumber - 1] = originalLines[lineNumber - 1].Insert(this.Extent.StartColumnNumber - 1, textReplacement);
173+
174+
var errorMessagePreFix = "Failed to apply AutoFix when writing to file " + scriptPath + Environment.NewLine;
175+
// we need to catch all exceptions that could be thrown except for ArgumentException and ArgumentNullException because at this stage the file path has already been verified.
176+
try
177+
{
178+
Console.WriteLine($"AutoFix {this.RuleName} by replacing '{textToBeReplaced}' with '{textReplacement}' in line {lineNumber} of file {scriptPath}");
179+
File.WriteAllLines(scriptPath, originalLines);
180+
}
181+
catch (PathTooLongException)
182+
{
183+
Console.WriteLine(errorMessagePreFix + "The specified path, file name, or both exceed the system - defined maximum length. " +
184+
"For example, on Windows - based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.");
185+
}
186+
catch (DirectoryNotFoundException)
187+
{
188+
Console.WriteLine(errorMessagePreFix + "The specified path is invalid (for example, it is on an unmapped drive).");
189+
}
190+
catch (IOException)
191+
{
192+
Console.WriteLine(errorMessagePreFix + "An I/O error occurred while opening the file.");
193+
}
194+
catch (UnauthorizedAccessException)
195+
{
196+
Console.WriteLine(errorMessagePreFix + "Path specified a file that is read-only or this operation is not supported on the current platform or the caller does not have the required permission.");
197+
}
198+
catch (SecurityException)
199+
{
200+
Console.WriteLine(errorMessagePreFix + "You do not have the required permission to write to the file.");
201+
}
202+
catch (Exception e)
203+
{
204+
Console.WriteLine(errorMessagePreFix + "Unexpected error:" + e.Message);
205+
}
206+
}
207+
else
208+
{
209+
Console.WriteLine("AutoFix functionality currently only supported for files");
210+
}
211+
}
134212
}
135213

136-
137214
/// <summary>
138215
/// Represents a severity level of an issue.
139216
/// </summary>

Engine/ScriptAnalyzer.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,8 +1454,9 @@ public Dictionary<string, List<string>> CheckRuleExtension(string[] path, PathIn
14541454
/// If true, recursively searches the given file path and analyzes any
14551455
/// script files that are found.
14561456
/// </param>
1457+
/// <param name="autoFix">Fix warnings that can be automatically fixed.</param>
14571458
/// <returns>An enumeration of DiagnosticRecords that were found by rules.</returns>
1458-
public IEnumerable<DiagnosticRecord> AnalyzePath(string path, bool searchRecursively = false)
1459+
public IEnumerable<DiagnosticRecord> AnalyzePath(string path, bool searchRecursively = false, bool autoFix = false)
14591460
{
14601461
List<string> scriptFilePaths = new List<string>();
14611462

@@ -1477,13 +1478,41 @@ public IEnumerable<DiagnosticRecord> AnalyzePath(string path, bool searchRecursi
14771478
{
14781479
// Yield each record in the result so that the
14791480
// caller can pull them one at a time
1480-
foreach (var diagnosticRecord in this.AnalyzeFile(scriptFilePath))
1481+
var diagnosticRecords = this.AnalyzeFile(scriptFilePath);
1482+
if(autoFix)
1483+
{
1484+
bool allFixableWarningsFixed = false;
1485+
while(!allFixableWarningsFixed)
1486+
{
1487+
allFixableWarningsFixed = !(AutoFixFirstFixableDiagnosticRecord(diagnosticRecords));
1488+
diagnosticRecords = this.AnalyzeFile(scriptFilePath); // update records with the correct line numbers after fix
1489+
}
1490+
}
1491+
foreach (var diagnosticRecord in diagnosticRecords)
14811492
{
14821493
yield return diagnosticRecord;
14831494
}
14841495
}
14851496
}
14861497

1498+
/// <summary>
1499+
/// Fixes only the first fixable DiagnosticRecord that it encounters because afterwards the other records need to be re-created as their line/column numbers are different then
1500+
/// </summary>
1501+
/// <param name="diagnosticRecords"></param>
1502+
/// <returns>True if it could fix a warning</returns>
1503+
private bool AutoFixFirstFixableDiagnosticRecord(IEnumerable<DiagnosticRecord> diagnosticRecords)
1504+
{
1505+
foreach(var diagnosticRecord in diagnosticRecords)
1506+
{
1507+
if (diagnosticRecord.CanBeFixedAutomatically)
1508+
{
1509+
diagnosticRecord.AutoFix();
1510+
return true;
1511+
}
1512+
}
1513+
return false;
1514+
}
1515+
14871516
/// <summary>
14881517
/// Analyzes a script definition in the form of a string input
14891518
/// </summary>

Rules/AvoidAlias.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
135135
DiagnosticSeverity.Warning,
136136
fileName,
137137
aliasName,
138-
suggestedCorrections: GetCorrectionExtent(cmdAst, cmdletName));
138+
suggestedCorrections: GetCorrectionExtent(cmdAst, cmdletName),
139+
canBeFixedAutomatically: true);
139140
}
140141
}
141142
}

0 commit comments

Comments
 (0)