Skip to content

Commit 2acd3aa

Browse files
committed
Refactor pester script detection
1 parent aa893b2 commit 2acd3aa

File tree

1 file changed

+97
-59
lines changed

1 file changed

+97
-59
lines changed

src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs

Lines changed: 97 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -27,63 +27,103 @@ IEnumerable<SymbolReference> IDocumentSymbolProvider.ProvideDocumentSymbols(
2727
return Enumerable.Empty<SymbolReference>();
2828
}
2929

30-
var commandAsts = scriptFile.ScriptAst.FindAll(ast =>
31-
{
32-
CommandAst commandAst = ast as CommandAst;
30+
// Find plausible Pester commands
31+
IEnumerable<Ast> commandAsts = scriptFile.ScriptAst.FindAll(IsNamedCommandWithArguments, true);
32+
33+
return commandAsts.OfType<CommandAst>()
34+
.Where(IsPesterCommand)
35+
.Select(ast => ConvertPesterAstToSymbolReference(scriptFile, ast))
36+
.Where(pesterSymbol => pesterSymbol?.TestName != null);
37+
}
38+
39+
/// <summary>
40+
/// Test if the given Ast is a regular CommandAst with arguments
41+
/// </summary>
42+
/// <param name="ast">the PowerShell Ast to test</param>
43+
/// <returns>true if the Ast represents a PowerShell command with arguments, false otherwise</returns>
44+
private static bool IsNamedCommandWithArguments(Ast ast)
45+
{
3346

3447
return
35-
commandAst != null &&
48+
ast is CommandAst commandAst &&
3649
commandAst.InvocationOperator != TokenKind.Dot &&
3750
PesterSymbolReference.GetCommandType(commandAst.GetCommandName()).HasValue &&
3851
commandAst.CommandElements.Count >= 2;
39-
},
40-
true);
52+
}
4153

42-
return commandAsts.Select(
43-
ast =>
44-
{
45-
// By this point we know the Ast is a CommandAst with 2 or more CommandElements
46-
int testNameParamIndex = 1;
47-
CommandAst testAst = (CommandAst)ast;
54+
/// <summary>
55+
/// Test whether the given CommandAst represents a Pester command
56+
/// </summary>
57+
/// <param name="commandAst">the CommandAst to test</param>
58+
/// <returns>true if the CommandAst represents a Pester command, false otherwise</returns>
59+
private static bool IsPesterCommand(CommandAst commandAst)
60+
{
61+
if (commandAst == null)
62+
{
63+
return false;
64+
}
4865

49-
// The -Name parameter
50-
for (int i = 1; i < testAst.CommandElements.Count; i++)
51-
{
52-
CommandParameterAst paramAst = testAst.CommandElements[i] as CommandParameterAst;
53-
if (paramAst != null &&
54-
paramAst.ParameterName.Equals("Name", StringComparison.OrdinalIgnoreCase))
55-
{
56-
testNameParamIndex = i + 1;
57-
break;
58-
}
59-
}
66+
// Ensure the first word is a Pester keyword
67+
if (!(commandAst.CommandElements[0] is StringConstantExpressionAst pesterKeywordAst &&
68+
PesterSymbolReference.PesterKeywords.ContainsKey(pesterKeywordAst.Value)))
69+
{
70+
return false;
71+
}
6072

61-
if (testNameParamIndex > testAst.CommandElements.Count - 1)
62-
{
63-
return null;
64-
}
73+
// Ensure that the last argument of the command is a scriptblock
74+
if (!(commandAst.CommandElements[commandAst.CommandElements.Count-1] is ScriptBlockExpressionAst))
75+
{
76+
return false;
77+
}
6578

66-
StringConstantExpressionAst stringAst =
67-
testAst.CommandElements[testNameParamIndex] as StringConstantExpressionAst;
79+
return true;
80+
}
81+
82+
/// <summary>
83+
/// Convert a CommandAst known to represent a Pester command and a reference to the scriptfile
84+
/// it is in into symbol representing a Pester call for code lens
85+
/// </summary>
86+
/// <param name="scriptFile">the scriptfile the Pester call occurs in</param>
87+
/// <param name="pesterCommandAst">the CommandAst representing the Pester call</param>
88+
/// <returns>a symbol representing the Pester call containing metadata for CodeLens to use</returns>
89+
private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFile scriptFile, CommandAst pesterCommandAst)
90+
{
91+
string testLine = scriptFile.GetLine(pesterCommandAst.Extent.StartLineNumber);
92+
string commandName = (pesterCommandAst.CommandElements[0] as StringConstantExpressionAst)?.Value;
6893

69-
if (stringAst == null)
94+
// Search for a name for the test
95+
string testName = null;
96+
for (int i = 1; i < pesterCommandAst.CommandElements.Count; i++)
97+
{
98+
CommandElementAst currentCommandElement = pesterCommandAst.CommandElements[i];
99+
100+
// Check for an explicit "-Name" parameter
101+
if (currentCommandElement is CommandParameterAst parameterAst)
102+
{
103+
i++;
104+
if (parameterAst.ParameterName == "Name" && i < pesterCommandAst.CommandElements.Count)
70105
{
71-
return null;
106+
testName = (pesterCommandAst.CommandElements[i] as StringConstantExpressionAst)?.Value;
107+
break;
72108
}
109+
continue;
110+
}
73111

74-
string testDefinitionLine =
75-
scriptFile.GetLine(
76-
ast.Extent.StartLineNumber);
77-
78-
return
79-
new PesterSymbolReference(
80-
scriptFile,
81-
testAst.GetCommandName(),
82-
testDefinitionLine,
83-
stringAst.Value,
84-
ast.Extent);
112+
// Otherwise, if an argument is given with no parameter, we assume it's the name
113+
if (pesterCommandAst.CommandElements[i] is StringConstantExpressionAst testNameStrAst)
114+
{
115+
testName = testNameStrAst.Value;
116+
break;
117+
}
118+
}
85119

86-
}).Where(s => s != null);
120+
return new PesterSymbolReference(
121+
scriptFile,
122+
commandName,
123+
testLine,
124+
testName,
125+
pesterCommandAst.Extent
126+
);
87127
}
88128
}
89129

@@ -114,6 +154,17 @@ public enum PesterCommandType
114154
/// </summary>
115155
public class PesterSymbolReference : SymbolReference
116156
{
157+
/// <summary>
158+
/// Lookup for Pester keywords we support. Ideally we could extract these from Pester itself
159+
/// </summary>
160+
internal static readonly IReadOnlyDictionary<string, PesterCommandType> PesterKeywords =
161+
new Dictionary<string, PesterCommandType>(StringComparer.OrdinalIgnoreCase)
162+
{
163+
{ "Describe", PesterCommandType.Describe },
164+
{ "Context", PesterCommandType.Context },
165+
{ "It", PesterCommandType.It }
166+
};
167+
117168
private static char[] DefinitionTrimChars = new char[] { ' ', '{' };
118169

119170
/// <summary>
@@ -145,25 +196,12 @@ internal PesterSymbolReference(
145196

146197
internal static PesterCommandType? GetCommandType(string commandName)
147198
{
148-
if (commandName == null)
199+
PesterCommandType pesterCommandType;
200+
if (!PesterKeywords.TryGetValue(commandName, out pesterCommandType))
149201
{
150202
return null;
151203
}
152-
153-
switch (commandName.ToLower())
154-
{
155-
case "describe":
156-
return PesterCommandType.Describe;
157-
158-
case "context":
159-
return PesterCommandType.Context;
160-
161-
case "it":
162-
return PesterCommandType.It;
163-
164-
default:
165-
return null;
166-
}
204+
return pesterCommandType;
167205
}
168206
}
169207
}

0 commit comments

Comments
 (0)