@@ -27,63 +27,106 @@ IEnumerable<SymbolReference> IDocumentSymbolProvider.ProvideDocumentSymbols(
27
27
return Enumerable . Empty < SymbolReference > ( ) ;
28
28
}
29
29
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
+ {
32
46
CommandAst commandAst = ast as CommandAst ;
33
47
34
48
return
35
49
commandAst != null &&
36
50
commandAst . InvocationOperator != TokenKind . Dot &&
37
51
PesterSymbolReference . GetCommandType ( commandAst . GetCommandName ( ) ) . HasValue &&
38
52
commandAst . CommandElements . Count >= 2 ;
39
- } ,
40
- true ) ;
53
+ }
41
54
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
+ }
48
66
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
+ }
60
72
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
+ }
65
81
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 ] ;
68
101
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 )
70
107
{
71
- return null ;
108
+ testName = alreadySawName ? null : ( pesterCommandAst . CommandElements [ i ] as StringConstantExpressionAst ) ? . Value ;
109
+ alreadySawName = true ;
72
110
}
111
+ continue ;
112
+ }
73
113
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
+ }
85
122
86
- } ) . Where ( s => s != null ) ;
123
+ return new PesterSymbolReference (
124
+ scriptFile ,
125
+ commandName ,
126
+ testLine ,
127
+ testName ,
128
+ pesterCommandAst . Extent
129
+ ) ;
87
130
}
88
131
}
89
132
@@ -114,6 +157,14 @@ public enum PesterCommandType
114
157
/// </summary>
115
158
public class PesterSymbolReference : SymbolReference
116
159
{
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
+
117
168
private static char [ ] DefinitionTrimChars = new char [ ] { ' ' , '{' } ;
118
169
119
170
/// <summary>
@@ -145,25 +196,12 @@ internal PesterSymbolReference(
145
196
146
197
internal static PesterCommandType ? GetCommandType ( string commandName )
147
198
{
148
- if ( commandName == null )
199
+ PesterCommandType pesterCommandType ;
200
+ if ( ! PesterKeywords . TryGetValue ( commandName , out pesterCommandType ) )
149
201
{
150
202
return null ;
151
203
}
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 ;
167
205
}
168
206
}
169
207
}
0 commit comments