@@ -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