From 393185f8e023bac3a9750f6dacdb9b4449bf62b3 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Fri, 9 Jun 2017 15:02:45 -0700 Subject: [PATCH] Fix crash and hang issues with Pester CodeLens feature This change fixes a couple of issues with the new Pester CodeLens feature. One is that the "Describe" function is not recognized when it is not cased exactly like that and the other is that the language server has an internal error that causes it to hang when typing Describe blocks into a script. Resolves PowerShell/vscode-powershell#850 Resolves PowerShell/vscode-powershell#851 Resolves PowerShell/vscode-powershell#852 --- .../CodeLens/PesterCodeLensProvider.cs | 21 +-- .../Symbols/PesterDocumentSymbolProvider.cs | 141 +++++++++++++++--- 2 files changed, 128 insertions(+), 34 deletions(-) diff --git a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs index 87e2c2d3e..61d700bb9 100644 --- a/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices.Host/CodeLens/PesterCodeLensProvider.cs @@ -27,18 +27,9 @@ public PesterCodeLensProvider(EditorSession editorSession) } private IEnumerable GetPesterLens( - SymbolReference symbol, + PesterSymbolReference pesterSymbol, ScriptFile scriptFile) { - // Trim the Describe "" { from the symbol name - int startQuoteIndex = symbol.SourceLine.IndexOfAny(QuoteChars) + 1; - int endQuoteIndex = symbol.SourceLine.LastIndexOfAny(QuoteChars); - - string describeBlockName = - symbol.SourceLine.Substring( - startQuoteIndex, - endQuoteIndex - startQuoteIndex); - var clientCommands = new ClientCommand[] { new ClientCommand( @@ -48,7 +39,7 @@ private IEnumerable GetPesterLens( { scriptFile.ClientFilePath, false, // Don't debug - describeBlockName, + pesterSymbol.TestName, }), new ClientCommand( @@ -58,7 +49,7 @@ private IEnumerable GetPesterLens( { scriptFile.ClientFilePath, true, // Run in debugger - describeBlockName, + pesterSymbol.TestName, }), }; @@ -68,7 +59,7 @@ private IEnumerable GetPesterLens( new CodeLens( this, scriptFile, - symbol.ScriptRegion, + pesterSymbol.ScriptRegion, command)); } @@ -80,8 +71,10 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile) var lenses = symbols - .Where(s => s.SymbolName.StartsWith("Describe")) + .OfType() + .Where(s => s.Command == PesterCommandType.Describe) .SelectMany(s => this.GetPesterLens(s, scriptFile)) + .Where(codeLens => codeLens != null) .ToArray(); return lenses; diff --git a/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs index 6eb7a2e66..9bb24e97e 100644 --- a/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs @@ -16,7 +16,6 @@ namespace Microsoft.PowerShell.EditorServices.Symbols /// public class PesterDocumentSymbolProvider : FeatureProviderBase, IDocumentSymbolProvider { - private static char[] DefinitionTrimChars = new char[] { ' ', '{' }; IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( ScriptFile scriptFile) @@ -30,33 +29,135 @@ IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( var commandAsts = scriptFile.ScriptAst.FindAll(ast => { - switch ((ast as CommandAst)?.GetCommandName()?.ToLower()) - { - case "describe": - case "context": - case "it": - return true; - - default: - return false; - } + CommandAst commandAst = ast as CommandAst; + + return + commandAst != null && + PesterSymbolReference.GetCommandType(commandAst.GetCommandName()).HasValue && + commandAst.CommandElements.Count >= 2; }, true); return commandAsts.Select( - ast => { - var testDefinitionLine = + ast => + { + // By this point we know the Ast is a CommandAst with 2 or more CommandElements + int testNameParamIndex = 1; + CommandAst testAst = (CommandAst)ast; + + // The -Name parameter + for (int i = 1; i < testAst.CommandElements.Count; i++) + { + CommandParameterAst paramAst = testAst.CommandElements[i] as CommandParameterAst; + if (paramAst != null && + paramAst.ParameterName.Equals("Name", StringComparison.OrdinalIgnoreCase)) + { + testNameParamIndex = i + 1; + break; + } + } + + if (testNameParamIndex > testAst.CommandElements.Count - 1) + { + return null; + } + + StringConstantExpressionAst stringAst = + testAst.CommandElements[testNameParamIndex] as StringConstantExpressionAst; + + if (stringAst == null) + { + return null; + } + + string testDefinitionLine = scriptFile.GetLine( ast.Extent.StartLineNumber); return - new SymbolReference( - SymbolType.Function, - testDefinitionLine.TrimEnd(DefinitionTrimChars), - ast.Extent, - scriptFile.FilePath, - testDefinitionLine); - }); + new PesterSymbolReference( + scriptFile, + testAst.GetCommandName(), + testDefinitionLine, + stringAst.Value, + ast.Extent); + + }).Where(s => s != null); + } + } + + /// + /// Defines command types for Pester test blocks. + /// + public enum PesterCommandType + { + /// + /// Identifies a Describe block. + /// + Describe, + + /// + /// Identifies a Context block. + /// + Context, + + /// + /// Identifies an It block. + /// + It + } + + /// + /// Provides a specialization of SymbolReference containing + /// extra information about Pester test symbols. + /// + public class PesterSymbolReference : SymbolReference + { + private static char[] DefinitionTrimChars = new char[] { ' ', '{' }; + + /// + /// Gets the name of the test + /// + public string TestName { get; private set; } + + /// + /// Gets the test's command type. + /// + public PesterCommandType Command { get; private set; } + + internal PesterSymbolReference( + ScriptFile scriptFile, + string commandName, + string testLine, + string testName, + IScriptExtent scriptExtent) + : base( + SymbolType.Function, + testLine.TrimEnd(DefinitionTrimChars), + scriptExtent, + scriptFile.FilePath, + testLine) + { + this.Command = GetCommandType(commandName).Value; + this.TestName = testName; + } + + internal static PesterCommandType? GetCommandType(string commandName) + { + switch (commandName.ToLower()) + { + case "describe": + return PesterCommandType.Describe; + + case "context": + return PesterCommandType.Context; + + case "it": + return PesterCommandType.It; + + default: + return null; + } } } }