@@ -27,63 +27,103 @@ IEnumerable<SymbolReference> IDocumentSymbolProvider.ProvideDocumentSymbols(
27
27
return Enumerable . Empty < SymbolReference > ( ) ;
28
28
}
29
29
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
+ {
33
46
34
47
return
35
- commandAst != null &&
48
+ ast is CommandAst commandAst &&
36
49
commandAst . InvocationOperator != TokenKind . Dot &&
37
50
PesterSymbolReference . GetCommandType ( commandAst . GetCommandName ( ) ) . HasValue &&
38
51
commandAst . CommandElements . Count >= 2 ;
39
- } ,
40
- true ) ;
52
+ }
41
53
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
+ }
48
65
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
+ }
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
+ }
65
78
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 ;
68
93
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 )
70
105
{
71
- return null ;
106
+ testName = ( pesterCommandAst . CommandElements [ i ] as StringConstantExpressionAst ) ? . Value ;
107
+ break ;
72
108
}
109
+ continue ;
110
+ }
73
111
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
+ }
85
119
86
- } ) . Where ( s => s != null ) ;
120
+ return new PesterSymbolReference (
121
+ scriptFile ,
122
+ commandName ,
123
+ testLine ,
124
+ testName ,
125
+ pesterCommandAst . Extent
126
+ ) ;
87
127
}
88
128
}
89
129
@@ -114,6 +154,17 @@ public enum PesterCommandType
114
154
/// </summary>
115
155
public class PesterSymbolReference : SymbolReference
116
156
{
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
+
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