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