66using System ;
77using System . Collections . Generic ;
88using System . Collections . Specialized ;
9+ using System . IO ;
910using System . Linq ;
1011using System . Management . Automation ;
12+ using System . Management . Automation . Language ;
1113using System . Runtime . InteropServices ;
14+ using System . Text . RegularExpressions ;
1215using System . Threading . Tasks ;
1316using Microsoft . Extensions . Logging ;
1417using Microsoft . PowerShell . EditorServices . Symbols ;
18+ using PowerShellEditorServices . Engine . Utility ;
1519
1620namespace Microsoft . PowerShell . EditorServices
1721{
@@ -25,6 +29,7 @@ public class SymbolsService
2529
2630 private readonly ILogger _logger ;
2731 private readonly PowerShellContextService _powerShellContextService ;
32+ private readonly WorkspaceService _workspaceService ;
2833 private readonly IDocumentSymbolProvider [ ] _documentSymbolProviders ;
2934
3035 #endregion
@@ -38,10 +43,12 @@ public class SymbolsService
3843 /// <param name="factory">An ILoggerFactory implementation used for writing log messages.</param>
3944 public SymbolsService (
4045 ILoggerFactory factory ,
41- PowerShellContextService powerShellContextService )
46+ PowerShellContextService powerShellContextService ,
47+ WorkspaceService workspaceService )
4248 {
4349 _logger = factory . CreateLogger < SymbolsService > ( ) ;
4450 _powerShellContextService = powerShellContextService ;
51+ _workspaceService = workspaceService ;
4552 _documentSymbolProviders = new IDocumentSymbolProvider [ ]
4653 {
4754 new ScriptDocumentSymbolProvider ( VersionUtils . PSVersion ) ,
@@ -320,5 +327,183 @@ await CommandHelpers.GetCommandInfoAsync(
320327 return null ;
321328 }
322329 }
330+
331+ /// <summary>
332+ /// Finds the definition of a symbol in the script file or any of the
333+ /// files that it references.
334+ /// </summary>
335+ /// <param name="sourceFile">The initial script file to be searched for the symbol's definition.</param>
336+ /// <param name="foundSymbol">The symbol for which a definition will be found.</param>
337+ /// <returns>The resulting GetDefinitionResult for the symbol's definition.</returns>
338+ public async Task < SymbolReference > GetDefinitionOfSymbolAsync (
339+ ScriptFile sourceFile ,
340+ SymbolReference foundSymbol )
341+ {
342+ Validate . IsNotNull ( nameof ( sourceFile ) , sourceFile ) ;
343+ Validate . IsNotNull ( nameof ( foundSymbol ) , foundSymbol ) ;
344+
345+ ScriptFile [ ] referencedFiles =
346+ _workspaceService . ExpandScriptReferences (
347+ sourceFile ) ;
348+
349+ var filesSearched = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
350+
351+ // look through the referenced files until definition is found
352+ // or there are no more file to look through
353+ SymbolReference foundDefinition = null ;
354+ foreach ( ScriptFile scriptFile in referencedFiles )
355+ {
356+ foundDefinition =
357+ AstOperations . FindDefinitionOfSymbol (
358+ scriptFile . ScriptAst ,
359+ foundSymbol ) ;
360+
361+ filesSearched . Add ( scriptFile . FilePath ) ;
362+ if ( foundDefinition != null )
363+ {
364+ foundDefinition . FilePath = scriptFile . FilePath ;
365+ break ;
366+ }
367+
368+ if ( foundSymbol . SymbolType == SymbolType . Function )
369+ {
370+ // Dot-sourcing is parsed as a "Function" Symbol.
371+ string dotSourcedPath = GetDotSourcedPath ( foundSymbol , scriptFile ) ;
372+ if ( scriptFile . FilePath == dotSourcedPath )
373+ {
374+ foundDefinition = new SymbolReference ( SymbolType . Function , foundSymbol . SymbolName , scriptFile . ScriptAst . Extent , scriptFile . FilePath ) ;
375+ break ;
376+ }
377+ }
378+ }
379+
380+ // if the definition the not found in referenced files
381+ // look for it in all the files in the workspace
382+ if ( foundDefinition == null )
383+ {
384+ // Get a list of all powershell files in the workspace path
385+ IEnumerable < string > allFiles = _workspaceService . EnumeratePSFiles ( ) ;
386+ foreach ( string file in allFiles )
387+ {
388+ if ( filesSearched . Contains ( file ) )
389+ {
390+ continue ;
391+ }
392+
393+ foundDefinition =
394+ AstOperations . FindDefinitionOfSymbol (
395+ Parser . ParseFile ( file , out Token [ ] tokens , out ParseError [ ] parseErrors ) ,
396+ foundSymbol ) ;
397+
398+ filesSearched . Add ( file ) ;
399+ if ( foundDefinition != null )
400+ {
401+ foundDefinition . FilePath = file ;
402+ break ;
403+ }
404+ }
405+ }
406+
407+ // if definition is not found in file in the workspace
408+ // look for it in the builtin commands
409+ if ( foundDefinition == null )
410+ {
411+ CommandInfo cmdInfo =
412+ await CommandHelpers . GetCommandInfoAsync (
413+ foundSymbol . SymbolName ,
414+ _powerShellContextService ) ;
415+
416+ foundDefinition =
417+ FindDeclarationForBuiltinCommand (
418+ cmdInfo ,
419+ foundSymbol ) ;
420+ }
421+
422+ return foundDefinition ;
423+ }
424+
425+ /// <summary>
426+ /// Gets a path from a dot-source symbol.
427+ /// </summary>
428+ /// <param name="symbol">The symbol representing the dot-source expression.</param>
429+ /// <param name="scriptFile">The script file containing the symbol</param>
430+ /// <returns></returns>
431+ private string GetDotSourcedPath ( SymbolReference symbol , ScriptFile scriptFile )
432+ {
433+ string cleanedUpSymbol = PathUtils . NormalizePathSeparators ( symbol . SymbolName . Trim ( '\' ' , '"' ) ) ;
434+ string psScriptRoot = Path . GetDirectoryName ( scriptFile . FilePath ) ;
435+ return _workspaceService . ResolveRelativeScriptPath ( psScriptRoot ,
436+ Regex . Replace ( cleanedUpSymbol , @"\$PSScriptRoot|\${PSScriptRoot}" , psScriptRoot , RegexOptions . IgnoreCase ) ) ;
437+ }
438+
439+ private SymbolReference FindDeclarationForBuiltinCommand (
440+ CommandInfo commandInfo ,
441+ SymbolReference foundSymbol )
442+ {
443+ if ( commandInfo == null )
444+ {
445+ return null ;
446+ }
447+
448+ ScriptFile [ ] nestedModuleFiles =
449+ GetBuiltinCommandScriptFiles (
450+ commandInfo . Module ) ;
451+
452+ SymbolReference foundDefinition = null ;
453+ foreach ( ScriptFile nestedModuleFile in nestedModuleFiles )
454+ {
455+ foundDefinition = AstOperations . FindDefinitionOfSymbol (
456+ nestedModuleFile . ScriptAst ,
457+ foundSymbol ) ;
458+
459+ if ( foundDefinition != null )
460+ {
461+ foundDefinition . FilePath = nestedModuleFile . FilePath ;
462+ break ;
463+ }
464+ }
465+
466+ return foundDefinition ;
467+ }
468+
469+ private ScriptFile [ ] GetBuiltinCommandScriptFiles (
470+ PSModuleInfo moduleInfo )
471+ {
472+ if ( moduleInfo == null )
473+ {
474+ return new ScriptFile [ 0 ] ;
475+ }
476+
477+ string modPath = moduleInfo . Path ;
478+ List < ScriptFile > scriptFiles = new List < ScriptFile > ( ) ;
479+ ScriptFile newFile ;
480+
481+ // find any files where the moduleInfo's path ends with ps1 or psm1
482+ // and add it to allowed script files
483+ if ( modPath . EndsWith ( @".ps1" , StringComparison . OrdinalIgnoreCase ) ||
484+ modPath . EndsWith ( @".psm1" , StringComparison . OrdinalIgnoreCase ) )
485+ {
486+ newFile = _workspaceService . GetFile ( modPath ) ;
487+ newFile . IsAnalysisEnabled = false ;
488+ scriptFiles . Add ( newFile ) ;
489+ }
490+
491+ if ( moduleInfo . NestedModules . Count > 0 )
492+ {
493+ foreach ( PSModuleInfo nestedInfo in moduleInfo . NestedModules )
494+ {
495+ string nestedModPath = nestedInfo . Path ;
496+ if ( nestedModPath . EndsWith ( @".ps1" , StringComparison . OrdinalIgnoreCase ) ||
497+ nestedModPath . EndsWith ( @".psm1" , StringComparison . OrdinalIgnoreCase ) )
498+ {
499+ newFile = _workspaceService . GetFile ( nestedModPath ) ;
500+ newFile . IsAnalysisEnabled = false ;
501+ scriptFiles . Add ( newFile ) ;
502+ }
503+ }
504+ }
505+
506+ return scriptFiles . ToArray ( ) ;
507+ }
323508 }
324509}
0 commit comments