Skip to content

Commit 9d5a2da

Browse files
authored
Merge pull request #638 from rjmholt/pester-tag-bugfix-1223
Refactor pester script detection to parse tags correctly
2 parents b95956a + 1e89761 commit 9d5a2da

File tree

1 file changed

+95
-57
lines changed

1 file changed

+95
-57
lines changed

src/PowerShellEditorServices/Symbols/PesterDocumentSymbolProvider.cs

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

30-
var commandAsts = scriptFile.ScriptAst.FindAll(ast =>
31-
{
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+
{
3246
CommandAst commandAst = ast as CommandAst;
3347

3448
return
3549
commandAst != null &&
3650
commandAst.InvocationOperator != TokenKind.Dot &&
3751
PesterSymbolReference.GetCommandType(commandAst.GetCommandName()).HasValue &&
3852
commandAst.CommandElements.Count >= 2;
39-
},
40-
true);
53+
}
4154

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;
55+
/// <summary>
56+
/// Test whether the given CommandAst represents a Pester command
57+
/// </summary>
58+
/// <param name="commandAst">the CommandAst to test</param>
59+
/// <returns>true if the CommandAst represents a Pester command, false otherwise</returns>
60+
private static bool IsPesterCommand(CommandAst commandAst)
61+
{
62+
if (commandAst == null)
63+
{
64+
return false;
65+
}
4866

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-
}
67+
// Ensure the first word is a Pester keyword
68+
if (!PesterSymbolReference.PesterKeywords.ContainsKey(commandAst.GetCommandName()))
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+
}
78+
79+
return true;
80+
}
6581

66-
StringConstantExpressionAst stringAst =
67-
testAst.CommandElements[testNameParamIndex] as StringConstantExpressionAst;
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.GetCommandName();
93+
94+
// Search for a name for the test
95+
// If the test has more than one argument for names, we set it to null
96+
string testName = null;
97+
bool alreadySawName = false;
98+
for (int i = 1; i < pesterCommandAst.CommandElements.Count; i++)
99+
{
100+
CommandElementAst currentCommandElement = pesterCommandAst.CommandElements[i];
68101

69-
if (stringAst == null)
102+
// Check for an explicit "-Name" parameter
103+
if (currentCommandElement is CommandParameterAst parameterAst)
104+
{
105+
i++;
106+
if (parameterAst.ParameterName == "Name" && i < pesterCommandAst.CommandElements.Count)
70107
{
71-
return null;
108+
testName = alreadySawName ? null : (pesterCommandAst.CommandElements[i] as StringConstantExpressionAst)?.Value;
109+
alreadySawName = true;
72110
}
111+
continue;
112+
}
73113

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);
114+
// Otherwise, if an argument is given with no parameter, we assume it's the name
115+
// If we've already seen a name, we set the name to null
116+
if (pesterCommandAst.CommandElements[i] is StringConstantExpressionAst testNameStrAst)
117+
{
118+
testName = alreadySawName ? null : testNameStrAst.Value;
119+
alreadySawName = true;
120+
}
121+
}
85122

86-
}).Where(s => s != null);
123+
return new PesterSymbolReference(
124+
scriptFile,
125+
commandName,
126+
testLine,
127+
testName,
128+
pesterCommandAst.Extent
129+
);
87130
}
88131
}
89132

@@ -114,6 +157,14 @@ public enum PesterCommandType
114157
/// </summary>
115158
public class PesterSymbolReference : SymbolReference
116159
{
160+
/// <summary>
161+
/// Lookup for Pester keywords we support. Ideally we could extract these from Pester itself
162+
/// </summary>
163+
internal static readonly IReadOnlyDictionary<string, PesterCommandType> PesterKeywords =
164+
Enum.GetValues(typeof(PesterCommandType))
165+
.Cast<PesterCommandType>()
166+
.ToDictionary(pct => pct.ToString(), pct => pct);
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)