Skip to content

Commit 8e29349

Browse files
authored
Fix formatter crash when script contains parse errors (#728)
* Fix formatter crash when script contains parse errors * Fix possible null references, add comments, improve variable types
1 parent d60e96f commit 8e29349

File tree

1 file changed

+127
-40
lines changed

1 file changed

+127
-40
lines changed

src/PowerShellEditorServices/Analysis/AnalysisService.cs

+127-40
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,48 @@ namespace Microsoft.PowerShell.EditorServices
2424
/// </summary>
2525
public class AnalysisService : IDisposable
2626
{
27+
#region Static fields
28+
29+
/// <summary>
30+
/// Defines the list of Script Analyzer rules to include by default if
31+
/// no settings file is specified.
32+
/// </summary>
33+
private static readonly string[] s_includedRules = {
34+
"PSUseToExportFieldsInManifest",
35+
"PSMisleadingBacktick",
36+
"PSAvoidUsingCmdletAliases",
37+
"PSUseApprovedVerbs",
38+
"PSAvoidUsingPlainTextForPassword",
39+
"PSReservedCmdletChar",
40+
"PSReservedParams",
41+
"PSShouldProcess",
42+
"PSMissingModuleManifestField",
43+
"PSAvoidDefaultValueSwitchParameter",
44+
"PSUseDeclaredVarsMoreThanAssignments",
45+
"PSPossibleIncorrectComparisonWithNull",
46+
"PSAvoidDefaultValueForMandatoryParameter",
47+
"PSPossibleIncorrectUsageOfRedirectionOperator"
48+
};
49+
50+
/// <summary>
51+
/// An empty diagnostic result to return when a script fails analysis.
52+
/// </summary>
53+
private static readonly PSObject[] s_emptyDiagnosticResult = new PSObject[0];
54+
55+
/// <summary>
56+
/// An empty script marker result to return when no script markers can be returned.
57+
/// </summary>
58+
private static readonly ScriptFileMarker[] s_emptyScriptMarkerResult = new ScriptFileMarker[0];
59+
60+
private static readonly string[] s_emptyGetRuleResult = new string[0];
61+
62+
/// <summary>
63+
/// The indentation to add when the logger lists errors.
64+
/// </summary>
65+
private static readonly string s_indentJoin = Environment.NewLine + " ";
66+
67+
#endregion // Static fields
68+
2769
#region Private Fields
2870

2971
/// <summary>
@@ -53,31 +95,8 @@ public class AnalysisService : IDisposable
5395
/// </summary>
5496
private PSModuleInfo _pssaModuleInfo;
5597

56-
/// <summary>
57-
/// Defines the list of Script Analyzer rules to include by default if
58-
/// no settings file is specified.
59-
/// </summary>
60-
private static readonly string[] s_includedRules = new string[]
61-
{
62-
"PSUseToExportFieldsInManifest",
63-
"PSMisleadingBacktick",
64-
"PSAvoidUsingCmdletAliases",
65-
"PSUseApprovedVerbs",
66-
"PSAvoidUsingPlainTextForPassword",
67-
"PSReservedCmdletChar",
68-
"PSReservedParams",
69-
"PSShouldProcess",
70-
"PSMissingModuleManifestField",
71-
"PSAvoidDefaultValueSwitchParameter",
72-
"PSUseDeclaredVarsMoreThanAssignments",
73-
"PSPossibleIncorrectComparisonWithNull",
74-
"PSAvoidDefaultValueForMandatoryParameter",
75-
"PSPossibleIncorrectUsageOfRedirectionOperator"
76-
};
77-
7898
#endregion // Private Fields
7999

80-
81100
#region Properties
82101

83102
/// <summary>
@@ -281,9 +300,15 @@ public async Task<ScriptFileMarker[]> GetSemanticMarkersAsync(
281300
/// </summary>
282301
public IEnumerable<string> GetPSScriptAnalyzerRules()
283302
{
284-
List<string> ruleNames = new List<string>();
285-
var ruleObjects = InvokePowerShell("Get-ScriptAnalyzerRule", new Dictionary<string, object>());
286-
foreach (var rule in ruleObjects)
303+
PowerShellResult getRuleResult = InvokePowerShell("Get-ScriptAnalyzerRule");
304+
if (getRuleResult == null)
305+
{
306+
_logger.Write(LogLevel.Warning, "Get-ScriptAnalyzerRule returned null result");
307+
return s_emptyGetRuleResult;
308+
}
309+
310+
var ruleNames = new List<string>();
311+
foreach (var rule in getRuleResult.Output)
287312
{
288313
ruleNames.Add((string)rule.Members["RuleName"].Value);
289314
}
@@ -319,8 +344,35 @@ public async Task<string> Format(
319344
argsDict.Add("Range", rangeList);
320345
}
321346

322-
var result = await InvokePowerShellAsync("Invoke-Formatter", argsDict);
323-
return result?.Select(r => r?.ImmediateBaseObject as string).FirstOrDefault();
347+
PowerShellResult result = await InvokePowerShellAsync("Invoke-Formatter", argsDict);
348+
349+
if (result == null)
350+
{
351+
_logger.Write(LogLevel.Error, "Formatter returned null result");
352+
return null;
353+
}
354+
355+
if (result.HasErrors)
356+
{
357+
var errorBuilder = new StringBuilder().Append(s_indentJoin);
358+
foreach (ErrorRecord err in result.Errors)
359+
{
360+
errorBuilder.Append(err).Append(s_indentJoin);
361+
}
362+
_logger.Write(LogLevel.Warning, $"Errors found while formatting file: {errorBuilder}");
363+
return null;
364+
}
365+
366+
foreach (PSObject resultObj in result.Output)
367+
{
368+
string formatResult = resultObj?.BaseObject as string;
369+
if (formatResult != null)
370+
{
371+
return formatResult;
372+
}
373+
}
374+
375+
return null;
324376
}
325377

326378
#endregion // public methods
@@ -342,7 +394,7 @@ private async Task<ScriptFileMarker[]> GetSemanticMarkersAsync<TSettings>(
342394
else
343395
{
344396
// Return an empty marker list
345-
return new ScriptFileMarker[0];
397+
return s_emptyScriptMarkerResult;
346398
}
347399
}
348400

@@ -360,7 +412,7 @@ private async Task<ScriptFileMarker[]> GetSemanticMarkersAsync<TSettings>(
360412
else
361413
{
362414
// Return an empty marker list
363-
return new ScriptFileMarker[0];
415+
return s_emptyScriptMarkerResult;
364416
}
365417
}
366418

@@ -380,11 +432,16 @@ private void LogAvailablePssaFeatures()
380432
// If we already know the module that was imported, save some work
381433
if (_pssaModuleInfo == null)
382434
{
383-
PSObject[] modules = InvokePowerShell(
435+
PowerShellResult getModuleResult = InvokePowerShell(
384436
"Get-Module",
385437
new Dictionary<string, object>{ {"Name", PSSA_MODULE_NAME} });
386438

387-
_pssaModuleInfo = modules
439+
if (getModuleResult == null)
440+
{
441+
throw new AnalysisServiceLoadException("Get-Module call to find PSScriptAnalyzer module failed");
442+
}
443+
444+
_pssaModuleInfo = getModuleResult.Output
388445
.Select(m => m.BaseObject)
389446
.OfType<PSModuleInfo>()
390447
.FirstOrDefault();
@@ -426,7 +483,7 @@ private async Task<PSObject[]> GetDiagnosticRecordsAsync<TSettings>(
426483
string[] rules,
427484
TSettings settings) where TSettings : class
428485
{
429-
var diagnosticRecords = new PSObject[0];
486+
var diagnosticRecords = s_emptyDiagnosticResult;
430487

431488
// When a new, empty file is created there are by definition no issues.
432489
// Furthermore, if you call Invoke-ScriptAnalyzer with an empty ScriptDefinition
@@ -452,13 +509,15 @@ private async Task<PSObject[]> GetDiagnosticRecordsAsync<TSettings>(
452509
settingArgument = rules;
453510
}
454511

455-
diagnosticRecords = await InvokePowerShellAsync(
512+
PowerShellResult result = await InvokePowerShellAsync(
456513
"Invoke-ScriptAnalyzer",
457514
new Dictionary<string, object>
458515
{
459516
{ "ScriptDefinition", scriptContent },
460517
{ settingParameter, settingArgument }
461518
});
519+
520+
diagnosticRecords = result?.Output;
462521
}
463522

464523
_logger.Write(
@@ -468,21 +527,26 @@ private async Task<PSObject[]> GetDiagnosticRecordsAsync<TSettings>(
468527
return diagnosticRecords;
469528
}
470529

471-
private PSObject[] InvokePowerShell(string command, IDictionary<string, object> paramArgMap)
530+
private PowerShellResult InvokePowerShell(string command, IDictionary<string, object> paramArgMap = null)
472531
{
473532
using (var powerShell = System.Management.Automation.PowerShell.Create())
474533
{
475534
powerShell.RunspacePool = _analysisRunspacePool;
476535
powerShell.AddCommand(command);
477-
foreach (var kvp in paramArgMap)
536+
if (paramArgMap != null)
478537
{
479-
powerShell.AddParameter(kvp.Key, kvp.Value);
538+
foreach (KeyValuePair<string, object> kvp in paramArgMap)
539+
{
540+
powerShell.AddParameter(kvp.Key, kvp.Value);
541+
}
480542
}
481543

482-
var result = new PSObject[0];
544+
PowerShellResult result = null;
483545
try
484546
{
485-
result = powerShell.Invoke()?.ToArray();
547+
PSObject[] output = powerShell.Invoke().ToArray();
548+
ErrorRecord[] errors = powerShell.Streams.Error.ToArray();
549+
result = new PowerShellResult(output, errors, powerShell.HadErrors);
486550
}
487551
catch (CommandNotFoundException ex)
488552
{
@@ -503,7 +567,7 @@ private PSObject[] InvokePowerShell(string command, IDictionary<string, object>
503567
}
504568
}
505569

506-
private async Task<PSObject[]> InvokePowerShellAsync(string command, IDictionary<string, object> paramArgMap)
570+
private async Task<PowerShellResult> InvokePowerShellAsync(string command, IDictionary<string, object> paramArgMap = null)
507571
{
508572
var task = Task.Run(() =>
509573
{
@@ -590,6 +654,29 @@ public void Dispose()
590654
}
591655

592656
#endregion
657+
658+
/// <summary>
659+
/// Wraps the result of an execution of PowerShell to send back through
660+
/// asynchronous calls.
661+
/// </summary>
662+
private class PowerShellResult
663+
{
664+
public PowerShellResult(
665+
PSObject[] output,
666+
ErrorRecord[] errors,
667+
bool hasErrors)
668+
{
669+
Output = output;
670+
Errors = errors;
671+
HasErrors = hasErrors;
672+
}
673+
674+
public PSObject[] Output { get; }
675+
676+
public ErrorRecord[] Errors { get; }
677+
678+
public bool HasErrors { get; }
679+
}
593680
}
594681

595682
/// <summary>

0 commit comments

Comments
 (0)