6
6
using System ;
7
7
using System . Collections . Generic ;
8
8
using System . Collections . Specialized ;
9
+ using System . IO ;
9
10
using System . Linq ;
10
11
using System . Management . Automation ;
12
+ using System . Management . Automation . Language ;
11
13
using System . Runtime . InteropServices ;
14
+ using System . Text . RegularExpressions ;
12
15
using System . Threading . Tasks ;
13
16
using Microsoft . Extensions . Logging ;
14
17
using Microsoft . PowerShell . EditorServices . Symbols ;
18
+ using PowerShellEditorServices . Engine . Utility ;
15
19
16
20
namespace Microsoft . PowerShell . EditorServices
17
21
{
@@ -25,6 +29,7 @@ public class SymbolsService
25
29
26
30
private readonly ILogger _logger ;
27
31
private readonly PowerShellContextService _powerShellContextService ;
32
+ private readonly WorkspaceService _workspaceService ;
28
33
private readonly IDocumentSymbolProvider [ ] _documentSymbolProviders ;
29
34
30
35
#endregion
@@ -38,10 +43,12 @@ public class SymbolsService
38
43
/// <param name="factory">An ILoggerFactory implementation used for writing log messages.</param>
39
44
public SymbolsService (
40
45
ILoggerFactory factory ,
41
- PowerShellContextService powerShellContextService )
46
+ PowerShellContextService powerShellContextService ,
47
+ WorkspaceService workspaceService )
42
48
{
43
49
_logger = factory . CreateLogger < SymbolsService > ( ) ;
44
50
_powerShellContextService = powerShellContextService ;
51
+ _workspaceService = workspaceService ;
45
52
_documentSymbolProviders = new IDocumentSymbolProvider [ ]
46
53
{
47
54
new ScriptDocumentSymbolProvider ( VersionUtils . PSVersion ) ,
@@ -320,5 +327,183 @@ await CommandHelpers.GetCommandInfoAsync(
320
327
return null ;
321
328
}
322
329
}
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
+ }
323
508
}
324
509
}
0 commit comments