diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs index 0d9763b9e..40b6327e5 100644 --- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs @@ -24,6 +24,48 @@ namespace Microsoft.PowerShell.EditorServices /// public class AnalysisService : IDisposable { + #region Static fields + + /// + /// Defines the list of Script Analyzer rules to include by default if + /// no settings file is specified. + /// + private static readonly string[] s_includedRules = { + "PSUseToExportFieldsInManifest", + "PSMisleadingBacktick", + "PSAvoidUsingCmdletAliases", + "PSUseApprovedVerbs", + "PSAvoidUsingPlainTextForPassword", + "PSReservedCmdletChar", + "PSReservedParams", + "PSShouldProcess", + "PSMissingModuleManifestField", + "PSAvoidDefaultValueSwitchParameter", + "PSUseDeclaredVarsMoreThanAssignments", + "PSPossibleIncorrectComparisonWithNull", + "PSAvoidDefaultValueForMandatoryParameter", + "PSPossibleIncorrectUsageOfRedirectionOperator" + }; + + /// + /// An empty diagnostic result to return when a script fails analysis. + /// + private static readonly PSObject[] s_emptyDiagnosticResult = new PSObject[0]; + + /// + /// An empty script marker result to return when no script markers can be returned. + /// + private static readonly ScriptFileMarker[] s_emptyScriptMarkerResult = new ScriptFileMarker[0]; + + private static readonly string[] s_emptyGetRuleResult = new string[0]; + + /// + /// The indentation to add when the logger lists errors. + /// + private static readonly string s_indentJoin = Environment.NewLine + " "; + + #endregion // Static fields + #region Private Fields /// @@ -53,31 +95,8 @@ public class AnalysisService : IDisposable /// private PSModuleInfo _pssaModuleInfo; - /// - /// Defines the list of Script Analyzer rules to include by default if - /// no settings file is specified. - /// - 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 /// @@ -281,9 +300,15 @@ public async Task GetSemanticMarkersAsync( /// public IEnumerable GetPSScriptAnalyzerRules() { - List ruleNames = new List(); - var ruleObjects = InvokePowerShell("Get-ScriptAnalyzerRule", new Dictionary()); - foreach (var rule in ruleObjects) + PowerShellResult getRuleResult = InvokePowerShell("Get-ScriptAnalyzerRule"); + if (getRuleResult == null) + { + _logger.Write(LogLevel.Warning, "Get-ScriptAnalyzerRule returned null result"); + return s_emptyGetRuleResult; + } + + var ruleNames = new List(); + foreach (var rule in getRuleResult.Output) { ruleNames.Add((string)rule.Members["RuleName"].Value); } @@ -319,8 +344,35 @@ public async Task 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"); + 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 +394,7 @@ private async Task GetSemanticMarkersAsync( else { // Return an empty marker list - return new ScriptFileMarker[0]; + return s_emptyScriptMarkerResult; } } @@ -360,7 +412,7 @@ private async Task GetSemanticMarkersAsync( else { // Return an empty marker list - return new ScriptFileMarker[0]; + return s_emptyScriptMarkerResult; } } @@ -380,11 +432,16 @@ private void LogAvailablePssaFeatures() // If we already know the module that was imported, save some work if (_pssaModuleInfo == null) { - PSObject[] modules = InvokePowerShell( + PowerShellResult getModuleResult = InvokePowerShell( "Get-Module", new Dictionary{ {"Name", PSSA_MODULE_NAME} }); - _pssaModuleInfo = modules + if (getModuleResult == null) + { + throw new AnalysisServiceLoadException("Get-Module call to find PSScriptAnalyzer module failed"); + } + + _pssaModuleInfo = getModuleResult.Output .Select(m => m.BaseObject) .OfType() .FirstOrDefault(); @@ -426,7 +483,7 @@ private async Task GetDiagnosticRecordsAsync( 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 +509,15 @@ private async Task GetDiagnosticRecordsAsync( settingArgument = rules; } - diagnosticRecords = await InvokePowerShellAsync( + PowerShellResult result = await InvokePowerShellAsync( "Invoke-ScriptAnalyzer", new Dictionary { { "ScriptDefinition", scriptContent }, { settingParameter, settingArgument } }); + + diagnosticRecords = result?.Output; } _logger.Write( @@ -468,21 +527,26 @@ private async Task GetDiagnosticRecordsAsync( return diagnosticRecords; } - private PSObject[] InvokePowerShell(string command, IDictionary paramArgMap) + private PowerShellResult InvokePowerShell(string command, IDictionary paramArgMap = null) { using (var powerShell = System.Management.Automation.PowerShell.Create()) { powerShell.RunspacePool = _analysisRunspacePool; powerShell.AddCommand(command); - foreach (var kvp in paramArgMap) + if (paramArgMap != null) { - powerShell.AddParameter(kvp.Key, kvp.Value); + foreach (KeyValuePair kvp in paramArgMap) + { + powerShell.AddParameter(kvp.Key, kvp.Value); + } } - var result = new PSObject[0]; + PowerShellResult result = null; try { - result = powerShell.Invoke()?.ToArray(); + PSObject[] output = powerShell.Invoke().ToArray(); + ErrorRecord[] errors = powerShell.Streams.Error.ToArray(); + result = new PowerShellResult(output, errors, powerShell.HadErrors); } catch (CommandNotFoundException ex) { @@ -503,7 +567,7 @@ private PSObject[] InvokePowerShell(string command, IDictionary } } - private async Task InvokePowerShellAsync(string command, IDictionary paramArgMap) + private async Task InvokePowerShellAsync(string command, IDictionary paramArgMap = null) { var task = Task.Run(() => { @@ -590,6 +654,29 @@ public void Dispose() } #endregion + + /// + /// Wraps the result of an execution of PowerShell to send back through + /// asynchronous calls. + /// + private class PowerShellResult + { + 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; } + } } ///