diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs
index 38a8956a3..9076b1cb9 100644
--- a/src/PowerShellEditorServices/Language/AstOperations.cs
+++ b/src/PowerShellEditorServices/Language/AstOperations.cs
@@ -321,10 +321,11 @@ static private bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap
/// Finds all files dot sourced in a script
///
/// The abstract syntax tree of the given script
+ /// Pre-calculated value of $PSScriptRoot
///
- static public string[] FindDotSourcedIncludes(Ast scriptAst)
+ static public string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot)
{
- FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor();
+ FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor(psScriptRoot);
scriptAst.Visit(dotSourcedVisitor);
return dotSourcedVisitor.DotSourcedFiles.ToArray();
diff --git a/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs
index 49d4d66ab..c0295a228 100644
--- a/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs
+++ b/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs
@@ -3,8 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
+using System;
using System.Collections.Generic;
using System.Management.Automation.Language;
+using Microsoft.PowerShell.EditorServices.Utility;
namespace Microsoft.PowerShell.EditorServices
{
@@ -13,14 +15,21 @@ namespace Microsoft.PowerShell.EditorServices
///
internal class FindDotSourcedVisitor : AstVisitor
{
- ///
- /// A hash set of the dot sourced files (because we don't want duplicates)
- ///
+ private readonly string _psScriptRoot;
+
+ ///
+ /// A hash set of the dot sourced files (because we don't want duplicates)
+ ///
public HashSet DotSourcedFiles { get; private set; }
- public FindDotSourcedVisitor()
+ ///
+ /// Creates a new instance of the FindDotSourcedVisitor class.
+ ///
+ /// Pre-calculated value of $PSScriptRoot
+ public FindDotSourcedVisitor(string psScriptRoot)
{
- this.DotSourcedFiles = new HashSet();
+ DotSourcedFiles = new HashSet(StringComparer.CurrentCultureIgnoreCase);
+ _psScriptRoot = psScriptRoot;
}
///
@@ -32,15 +41,50 @@ public FindDotSourcedVisitor()
/// or a decision to continue if it wasn't found
public override AstVisitAction VisitCommand(CommandAst commandAst)
{
- if (commandAst.InvocationOperator.Equals(TokenKind.Dot) &&
- commandAst.CommandElements[0] is StringConstantExpressionAst)
+ CommandElementAst commandElementAst = commandAst.CommandElements[0];
+ if (commandAst.InvocationOperator.Equals(TokenKind.Dot))
{
- // Strip any quote characters off of the string
- string fileName = commandAst.CommandElements[0].Extent.Text.Trim('\'', '"');
- DotSourcedFiles.Add(fileName);
+ string path;
+ switch (commandElementAst)
+ {
+ case StringConstantExpressionAst stringConstantExpressionAst:
+ path = stringConstantExpressionAst.Value;
+ break;
+
+ case ExpandableStringExpressionAst expandableStringExpressionAst:
+ path = GetPathFromExpandableStringExpression(expandableStringExpressionAst);
+ break;
+
+ default:
+ path = null;
+ break;
+ }
+
+ if (!string.IsNullOrWhiteSpace(path))
+ {
+ DotSourcedFiles.Add(PathUtils.NormalizePathSeparators(path));
+ }
}
return base.VisitCommand(commandAst);
}
+
+ private string GetPathFromExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst)
+ {
+ var path = expandableStringExpressionAst.Value;
+ foreach (var nestedExpression in expandableStringExpressionAst.NestedExpressions)
+ {
+ // If the string contains the variable $PSScriptRoot, we replace it with the corresponding value.
+ if (!(nestedExpression is VariableExpressionAst variableAst
+ && variableAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.OrdinalIgnoreCase)))
+ {
+ return null; // We return null instead of a partially evaluated ExpandableStringExpression.
+ }
+
+ path = path.Replace(variableAst.ToString(), _psScriptRoot);
+ }
+
+ return path;
+ }
}
}
diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs
index f721bd1f5..92964e1b6 100644
--- a/src/PowerShellEditorServices/Language/LanguageService.cs
+++ b/src/PowerShellEditorServices/Language/LanguageService.cs
@@ -14,6 +14,7 @@
using System.Management.Automation.Language;
using System.Runtime.InteropServices;
using System.Security;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -409,19 +410,30 @@ public async Task GetDefinitionOfSymbol(
// look through the referenced files until definition is found
// or there are no more file to look through
SymbolReference foundDefinition = null;
- for (int i = 0; i < referencedFiles.Length; i++)
+ foreach (ScriptFile scriptFile in referencedFiles)
{
foundDefinition =
AstOperations.FindDefinitionOfSymbol(
- referencedFiles[i].ScriptAst,
+ scriptFile.ScriptAst,
foundSymbol);
- filesSearched.Add(referencedFiles[i].FilePath);
+ filesSearched.Add(scriptFile.FilePath);
if (foundDefinition != null)
{
- foundDefinition.FilePath = referencedFiles[i].FilePath;
+ foundDefinition.FilePath = scriptFile.FilePath;
break;
}
+
+ if (foundSymbol.SymbolType == SymbolType.Function)
+ {
+ // Dot-sourcing is parsed as a "Function" Symbol.
+ string dotSourcedPath = GetDotSourcedPath(foundSymbol, workspace, scriptFile);
+ if (scriptFile.FilePath == dotSourcedPath)
+ {
+ foundDefinition = new SymbolReference(SymbolType.Function, foundSymbol.SymbolName, scriptFile.ScriptAst.Extent, scriptFile.FilePath);
+ break;
+ }
+ }
}
// if the definition the not found in referenced files
@@ -475,6 +487,21 @@ await CommandHelpers.GetCommandInfo(
null;
}
+ ///
+ /// Gets a path from a dot-source symbol.
+ ///
+ /// The symbol representing the dot-source expression.
+ /// The current workspace
+ /// The script file containing the symbol
+ ///
+ private static string GetDotSourcedPath(SymbolReference symbol, Workspace workspace, ScriptFile scriptFile)
+ {
+ string cleanedUpSymbol = PathUtils.NormalizePathSeparators(symbol.SymbolName.Trim('\'', '"'));
+ string psScriptRoot = Path.GetDirectoryName(scriptFile.FilePath);
+ return workspace.ResolveRelativeScriptPath(psScriptRoot,
+ Regex.Replace(cleanedUpSymbol, @"\$PSScriptRoot|\${PSScriptRoot}", psScriptRoot, RegexOptions.IgnoreCase));
+ }
+
///
/// Finds all the occurences of a symbol in the script given a file location
///
@@ -712,7 +739,7 @@ await _powerShellContext.GetRunspaceHandle(
{
if (!_cmdletToAliasDictionary.ContainsKey(aliasInfo.Definition))
{
- _cmdletToAliasDictionary.Add(aliasInfo.Definition, new List{ aliasInfo.Name });
+ _cmdletToAliasDictionary.Add(aliasInfo.Definition, new List { aliasInfo.Name });
}
else
{
diff --git a/src/PowerShellEditorServices/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs
new file mode 100644
index 000000000..6b62122ec
--- /dev/null
+++ b/src/PowerShellEditorServices/Utility/PathUtils.cs
@@ -0,0 +1,49 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.PowerShell.EditorServices.Utility
+{
+ ///
+ /// Utility to help handling paths across different platforms.
+ ///
+ ///
+ /// Some constants were copied from the internal System.Management.Automation.StringLiterals class.
+ ///
+ internal static class PathUtils
+ {
+ ///
+ /// The default path separator used by the base implementation of the providers.
+ ///
+ /// Porting note: IO.Path.DirectorySeparatorChar is correct for all platforms. On Windows,
+ /// it is '\', and on Linux, it is '/', as expected.
+ ///
+ internal static readonly char DefaultPathSeparator = Path.DirectorySeparatorChar;
+ internal static readonly string DefaultPathSeparatorString = DefaultPathSeparator.ToString();
+
+ ///
+ /// The alternate path separator used by the base implementation of the providers.
+ ///
+ /// Porting note: we do not use .NET's AlternatePathSeparatorChar here because it correctly
+ /// states that both the default and alternate are '/' on Linux. However, for PowerShell to
+ /// be "slash agnostic", we need to use the assumption that a '\' is the alternate path
+ /// separator on Linux.
+ ///
+ internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\';
+ internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString();
+
+ ///
+ /// Converts all alternate path separators to the current platform's main path separators.
+ ///
+ /// The path to normalize.
+ /// The normalized path.
+ public static string NormalizePathSeparators(string path)
+ {
+ return string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator);
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Workspace/ScriptFile.cs b/src/PowerShellEditorServices/Workspace/ScriptFile.cs
index 4851af99c..a81200887 100644
--- a/src/PowerShellEditorServices/Workspace/ScriptFile.cs
+++ b/src/PowerShellEditorServices/Workspace/ScriptFile.cs
@@ -20,7 +20,7 @@ public class ScriptFile
{
#region Private Fields
- private static readonly string[] s_newlines = new []
+ private static readonly string[] s_newlines = new[]
{
"\r\n",
"\n"
@@ -649,8 +649,7 @@ private void ParseFileContents()
.ToArray();
//Get all dot sourced referenced files and store them
- this.ReferencedFiles =
- AstOperations.FindDotSourcedIncludes(this.ScriptAst);
+ this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst, Path.GetDirectoryName(this.FilePath));
}
#endregion
diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs
index b53832f07..69847ee4d 100644
--- a/src/PowerShellEditorServices/Workspace/Workspace.cs
+++ b/src/PowerShellEditorServices/Workspace/Workspace.cs
@@ -530,7 +530,7 @@ private string GetBaseFilePath(string filePath)
return Path.GetDirectoryName(filePath);
}
- private string ResolveRelativeScriptPath(string baseFilePath, string relativePath)
+ internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath)
{
string combinedPath = null;
Exception resolveException = null;
diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs
new file mode 100644
index 000000000..20160081a
--- /dev/null
+++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs
@@ -0,0 +1,20 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.PowerShell.EditorServices;
+
+namespace Microsoft.PowerShell.EditorServices.Test.Shared.Definition
+{
+ public class FindsDotSourcedFile
+ {
+ public static readonly ScriptRegion SourceDetails =
+ new ScriptRegion
+ {
+ File = @"References\DotSources.ps1",
+ StartLineNumber = 1,
+ StartColumnNumber = 3
+ };
+ }
+}
diff --git a/test/PowerShellEditorServices.Test.Shared/References/DotSources.ps1 b/test/PowerShellEditorServices.Test.Shared/References/DotSources.ps1
new file mode 100644
index 000000000..685183ea6
--- /dev/null
+++ b/test/PowerShellEditorServices.Test.Shared/References/DotSources.ps1
@@ -0,0 +1,15 @@
+. ./ReferenceFileE.ps1
+. "$PSScriptRoot/ReferenceFileE.ps1"
+. "${PSScriptRoot}/ReferenceFileE.ps1"
+. './ReferenceFileE.ps1'
+. "./ReferenceFileE.ps1"
+. .\ReferenceFileE.ps1
+. '.\ReferenceFileE.ps1'
+. ".\ReferenceFileE.ps1"
+. ReferenceFileE.ps1
+. 'ReferenceFileE.ps1'
+. "ReferenceFileE.ps1"
+. ./dir/../ReferenceFileE.ps1
+. ./invalidfile.ps1
+. ""
+. $someVar
diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1
index add70c1d6..980ed33da 100644
--- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1
+++ b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1
@@ -1,5 +1,5 @@
-. .\ReferenceFileC.ps1
+. "$PSScriptRoot\ReferenceFileC.ps1"
Get-ChildItem
-My-Function "testb"
\ No newline at end of file
+My-Function "testb"
diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs
index 1e19a544a..a432fb73f 100644
--- a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs
+++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs
@@ -158,6 +158,23 @@ await this.GetDefinition(
Assert.Equal("My-Function", definition.SymbolName);
}
+ [Fact]
+ public async Task LanguageServiceFindsDotSourcedFile()
+ {
+ GetDefinitionResult definitionResult =
+ await this.GetDefinition(
+ FindsDotSourcedFile.SourceDetails);
+
+ SymbolReference definition = definitionResult.FoundDefinition;
+ Assert.True(
+ definitionResult.FoundDefinition.FilePath.EndsWith(
+ Path.Combine("References", "ReferenceFileE.ps1")),
+ "Unexpected reference file: " + definitionResult.FoundDefinition.FilePath);
+ Assert.Equal(1, definition.ScriptRegion.StartLineNumber);
+ Assert.Equal(1, definition.ScriptRegion.StartColumnNumber);
+ Assert.Equal("./ReferenceFileE.ps1", definition.SymbolName);
+ }
+
[Fact]
public async Task LanguageServiceFindsFunctionDefinitionInWorkspace()
{