-
Notifications
You must be signed in to change notification settings - Fork 237
Fix formatter crash when script contains parse errors #728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,47 @@ namespace Microsoft.PowerShell.EditorServices | |
/// </summary> | ||
public class AnalysisService : IDisposable | ||
{ | ||
#region Static fields | ||
|
||
/// <summary> | ||
/// Defines the list of Script Analyzer rules to include by default if | ||
/// no settings file is specified. | ||
/// </summary> | ||
private static readonly string[] s_includedRules = new string[] | ||
{ | ||
"PSUseToExportFieldsInManifest", | ||
"PSMisleadingBacktick", | ||
"PSAvoidUsingCmdletAliases", | ||
"PSUseApprovedVerbs", | ||
"PSAvoidUsingPlainTextForPassword", | ||
"PSReservedCmdletChar", | ||
"PSReservedParams", | ||
"PSShouldProcess", | ||
"PSMissingModuleManifestField", | ||
"PSAvoidDefaultValueSwitchParameter", | ||
"PSUseDeclaredVarsMoreThanAssignments", | ||
"PSPossibleIncorrectComparisonWithNull", | ||
"PSAvoidDefaultValueForMandatoryParameter", | ||
"PSPossibleIncorrectUsageOfRedirectionOperator" | ||
}; | ||
|
||
/// <summary> | ||
/// An empty diagnostic result to return when a script fails analysis. | ||
/// </summary> | ||
private static readonly PSObject[] s_emptyDiagnosticResult = new PSObject[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer that -- so long as .NET 4.5.1 supports that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like it doesn't. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aww :/ Probably not worth setting up then since we'll get it when we move to standard. |
||
|
||
/// <summary> | ||
/// An empty script marker result to return when no script markers can be returned. | ||
/// </summary> | ||
private static readonly ScriptFileMarker[] s_emptyScriptMarkerResult = new ScriptFileMarker[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. echo: |
||
|
||
/// <summary> | ||
/// The indentation to add when the logger lists errors. | ||
/// </summary> | ||
private static readonly string s_indentJoin = Environment.NewLine + " "; | ||
|
||
#endregion // Static fields | ||
|
||
#region Private Fields | ||
|
||
/// <summary> | ||
|
@@ -53,31 +94,8 @@ public class AnalysisService : IDisposable | |
/// </summary> | ||
private PSModuleInfo _pssaModuleInfo; | ||
|
||
/// <summary> | ||
/// Defines the list of Script Analyzer rules to include by default if | ||
/// no settings file is specified. | ||
/// </summary> | ||
private static readonly string[] s_includedRules = new string[] | ||
{ | ||
"PSUseToExportFieldsInManifest", | ||
"PSMisleadingBacktick", | ||
"PSAvoidUsingCmdletAliases", | ||
"PSUseApprovedVerbs", | ||
"PSAvoidUsingPlainTextForPassword", | ||
"PSReservedCmdletChar", | ||
"PSReservedParams", | ||
"PSShouldProcess", | ||
"PSMissingModuleManifestField", | ||
"PSAvoidDefaultValueSwitchParameter", | ||
"PSUseDeclaredVarsMoreThanAssignments", | ||
"PSPossibleIncorrectComparisonWithNull", | ||
"PSAvoidDefaultValueForMandatoryParameter", | ||
"PSPossibleIncorrectUsageOfRedirectionOperator" | ||
}; | ||
|
||
#endregion // Private Fields | ||
|
||
|
||
#region Properties | ||
|
||
/// <summary> | ||
|
@@ -281,8 +299,8 @@ public async Task<ScriptFileMarker[]> GetSemanticMarkersAsync( | |
/// </summary> | ||
public IEnumerable<string> GetPSScriptAnalyzerRules() | ||
{ | ||
List<string> ruleNames = new List<string>(); | ||
var ruleObjects = InvokePowerShell("Get-ScriptAnalyzerRule", new Dictionary<string, object>()); | ||
var ruleNames = new List<string>(); | ||
IEnumerable<PSObject> ruleObjects = InvokePowerShell("Get-ScriptAnalyzerRule", new Dictionary<string, object>()).Output; | ||
foreach (var rule in ruleObjects) | ||
{ | ||
ruleNames.Add((string)rule.Members["RuleName"].Value); | ||
|
@@ -319,8 +337,35 @@ public async Task<string> Format( | |
argsDict.Add("Range", rangeList); | ||
} | ||
|
||
var result = await InvokePowerShellAsync("Invoke-Formatter", argsDict); | ||
return result?.Select(r => r?.ImmediateBaseObject as string).FirstOrDefault(); | ||
PowerShellResult result = await InvokePowerShellAsync("Invoke-Formatter", argsDict); | ||
|
||
if (result == null) | ||
{ | ||
_logger.Write(LogLevel.Error, $"Formatter returned null result"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: do you need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah started out writing a different message |
||
return null; | ||
} | ||
|
||
if (result.HasErrors) | ||
{ | ||
var errorBuilder = new StringBuilder().Append(s_indentJoin); | ||
foreach (ErrorRecord err in result.Errors) | ||
{ | ||
errorBuilder.Append(err).Append(s_indentJoin); | ||
} | ||
_logger.Write(LogLevel.Warning, $"Errors found while formatting file: {errorBuilder}"); | ||
return null; | ||
} | ||
|
||
foreach (PSObject resultObj in result.Output) | ||
{ | ||
string formatResult = resultObj?.BaseObject as string; | ||
if (formatResult != null) | ||
{ | ||
return formatResult; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
#endregion // public methods | ||
|
@@ -342,7 +387,7 @@ private async Task<ScriptFileMarker[]> GetSemanticMarkersAsync<TSettings>( | |
else | ||
{ | ||
// Return an empty marker list | ||
return new ScriptFileMarker[0]; | ||
return s_emptyScriptMarkerResult; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
} | ||
} | ||
|
||
|
@@ -360,7 +405,7 @@ private async Task<ScriptFileMarker[]> GetSemanticMarkersAsync<TSettings>( | |
else | ||
{ | ||
// Return an empty marker list | ||
return new ScriptFileMarker[0]; | ||
return s_emptyScriptMarkerResult; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
} | ||
} | ||
|
||
|
@@ -382,7 +427,7 @@ private void LogAvailablePssaFeatures() | |
{ | ||
PSObject[] modules = InvokePowerShell( | ||
"Get-Module", | ||
new Dictionary<string, object>{ {"Name", PSSA_MODULE_NAME} }); | ||
new Dictionary<string, object>{ {"Name", PSSA_MODULE_NAME} })?.Output; | ||
|
||
_pssaModuleInfo = modules | ||
.Select(m => m.BaseObject) | ||
|
@@ -426,7 +471,7 @@ private async Task<PSObject[]> GetDiagnosticRecordsAsync<TSettings>( | |
string[] rules, | ||
TSettings settings) where TSettings : class | ||
{ | ||
var diagnosticRecords = new PSObject[0]; | ||
var diagnosticRecords = s_emptyDiagnosticResult; | ||
|
||
// When a new, empty file is created there are by definition no issues. | ||
// Furthermore, if you call Invoke-ScriptAnalyzer with an empty ScriptDefinition | ||
|
@@ -452,13 +497,15 @@ private async Task<PSObject[]> GetDiagnosticRecordsAsync<TSettings>( | |
settingArgument = rules; | ||
} | ||
|
||
diagnosticRecords = await InvokePowerShellAsync( | ||
PowerShellResult result = await InvokePowerShellAsync( | ||
"Invoke-ScriptAnalyzer", | ||
new Dictionary<string, object> | ||
{ | ||
{ "ScriptDefinition", scriptContent }, | ||
{ settingParameter, settingArgument } | ||
}); | ||
|
||
diagnosticRecords = result?.Output; | ||
} | ||
|
||
_logger.Write( | ||
|
@@ -468,7 +515,7 @@ private async Task<PSObject[]> GetDiagnosticRecordsAsync<TSettings>( | |
return diagnosticRecords; | ||
} | ||
|
||
private PSObject[] InvokePowerShell(string command, IDictionary<string, object> paramArgMap) | ||
private PowerShellResult InvokePowerShell(string command, IDictionary<string, object> paramArgMap) | ||
{ | ||
using (var powerShell = System.Management.Automation.PowerShell.Create()) | ||
{ | ||
|
@@ -479,10 +526,12 @@ private PSObject[] InvokePowerShell(string command, IDictionary<string, object> | |
powerShell.AddParameter(kvp.Key, kvp.Value); | ||
} | ||
|
||
var result = new PSObject[0]; | ||
PowerShellResult result = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be set to null? Just want to make sure we don't cause a null pointer exception somewhere else :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it has to be set to something here, and creating a new PowerShell result would require creating two new arrays as well. Doesn't seem right. I think I've dealt with the possibility that any result here could return But, perhaps it's better to do something here like throw an analysis exception, or something else we catch?? |
||
try | ||
{ | ||
result = powerShell.Invoke()?.ToArray(); | ||
PSObject[] output = powerShell.Invoke()?.ToArray(); | ||
ErrorRecord[] errors = powerShell.Streams?.Error?.ToArray(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be able to drop the null conditionals, as far as I know Come to think of it, I don't think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what I was hoping for |
||
result = new PowerShellResult(output, errors, powerShell.HadErrors); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I notice that both output and errors could potentially be set to null here... Is that going to be ok? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we think they can't actually be |
||
} | ||
catch (CommandNotFoundException ex) | ||
{ | ||
|
@@ -503,7 +552,7 @@ private PSObject[] InvokePowerShell(string command, IDictionary<string, object> | |
} | ||
} | ||
|
||
private async Task<PSObject[]> InvokePowerShellAsync(string command, IDictionary<string, object> paramArgMap) | ||
private async Task<PowerShellResult> InvokePowerShellAsync(string command, IDictionary<string, object> paramArgMap) | ||
{ | ||
var task = Task.Run(() => | ||
{ | ||
|
@@ -590,6 +639,29 @@ public void Dispose() | |
} | ||
|
||
#endregion | ||
|
||
/// <summary> | ||
/// Wraps the result of an execution of PowerShell to send back through | ||
/// asynchronous calls. | ||
/// </summary> | ||
private class PowerShellResult | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this class could be put in some utility class rather than in this file - perhaps if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, but |
||
{ | ||
public PowerShellResult( | ||
PSObject[] output, | ||
ErrorRecord[] errors, | ||
bool hasErrors) | ||
{ | ||
Output = output; | ||
Errors = errors; | ||
HasErrors = hasErrors; | ||
} | ||
|
||
public PSObject[] Output { get; } | ||
|
||
public ErrorRecord[] Errors { get; } | ||
|
||
public bool HasErrors { get; } | ||
} | ||
} | ||
|
||
/// <summary> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't know how everyone feels about this, but you can ditch the
new string[]
and just keep the braces for a field initialization. e.gThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh!