-
Notifications
You must be signed in to change notification settings - Fork 237
Fix #17: Add go to definition support for dot sourced file paths #786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
c1568fa
d865f3e
a2fc548
c774e7c
7507fec
11a3107
5cd5061
0c1ea1f
49ec4a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,12 @@ | |
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
// | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Management.Automation.Language; | ||
using System.Text.RegularExpressions; | ||
dee-see marked this conversation as resolved.
Show resolved
Hide resolved
|
||
using Microsoft.PowerShell.EditorServices.Utility; | ||
|
||
namespace Microsoft.PowerShell.EditorServices | ||
{ | ||
|
@@ -13,14 +17,17 @@ namespace Microsoft.PowerShell.EditorServices | |
/// </summary> | ||
internal class FindDotSourcedVisitor : AstVisitor | ||
{ | ||
/// <summary> | ||
/// A hash set of the dot sourced files (because we don't want duplicates) | ||
/// </summary> | ||
private readonly string _scriptDirectory; | ||
|
||
/// <summary> | ||
/// A hash set of the dot sourced files (because we don't want duplicates) | ||
/// </summary> | ||
public HashSet<string> DotSourcedFiles { get; private set; } | ||
|
||
public FindDotSourcedVisitor() | ||
public FindDotSourcedVisitor(string scriptPath) | ||
dee-see marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
this.DotSourcedFiles = new HashSet<string>(); | ||
DotSourcedFiles = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase); | ||
_scriptDirectory = Path.GetDirectoryName(scriptPath); | ||
} | ||
|
||
/// <summary> | ||
|
@@ -32,15 +39,44 @@ public FindDotSourcedVisitor() | |
/// or a decision to continue if it wasn't found</returns> | ||
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); | ||
if (commandElementAst is StringConstantExpressionAst stringConstantExpressionAst) | ||
dee-see marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
// Strip any quote characters off of the string | ||
DotSourcedFiles.Add(PathUtils.NormalizePathSeparators(stringConstantExpressionAst.Value)); | ||
} | ||
else if (commandElementAst is ExpandableStringExpressionAst expandableStringExpressionAst) | ||
{ | ||
var path = GetPathFromExpandableStringExpression(expandableStringExpressionAst); | ||
if (path != null) | ||
{ | ||
DotSourcedFiles.Add(PathUtils.NormalizePathSeparators(path)); | ||
} | ||
} | ||
} | ||
|
||
return base.VisitCommand(commandAst); | ||
} | ||
|
||
private string GetPathFromExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) | ||
{ | ||
var path = expandableStringExpressionAst.Value; | ||
foreach (var nestedExpression in expandableStringExpressionAst.NestedExpressions) | ||
dee-see marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (nestedExpression is VariableExpressionAst variableExpressionAst | ||
&& variableExpressionAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.CurrentCultureIgnoreCase)) | ||
dee-see marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
path = path.Replace(variableExpressionAst.ToString(), _scriptDirectory); | ||
} | ||
else | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than have an if (!(nestedExpression is VariableExpressionAst variableAst
&& variableAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.OrdinalIgnoreCase)))
{
return null;
}
path = path.Replace(variableAst.ToString(), _psScriptRoot); |
||
{ | ||
return null; // We're going to get an invalid path anyway. | ||
} | ||
} | ||
|
||
return path; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<GetDefinitionResult> 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,20 @@ await CommandHelpers.GetCommandInfo( | |
null; | ||
} | ||
|
||
/// <summary> | ||
/// Gets a path from a dot-source symbol. | ||
/// </summary> | ||
/// <param name="symbol">The symbol representing the dot-source expression.</param> | ||
/// <param name="workspace">The current workspace</param> | ||
/// <param name="scriptFile">The script file containing the symbol</param> | ||
/// <returns></returns> | ||
private static string GetDotSourcedPath(SymbolReference symbol, Workspace workspace, ScriptFile scriptFile) | ||
{ | ||
string cleanedUpSymbol = PathUtils.NormalizePathSeparators(symbol.SymbolName.Trim('\'', '"')); | ||
return workspace.ResolveRelativeScriptPath(Path.GetDirectoryName(scriptFile.FilePath), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth factoring out the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooop yes definitely, that was sloppy. |
||
Regex.Replace(cleanedUpSymbol, @"\$PSScriptRoot", Path.GetDirectoryName(scriptFile.FilePath), RegexOptions.IgnoreCase)); | ||
dee-see marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// <summary> | ||
/// Finds all the occurences of a symbol in the script given a file location | ||
/// </summary> | ||
|
@@ -712,7 +738,7 @@ await _powerShellContext.GetRunspaceHandle( | |
{ | ||
if (!_cmdletToAliasDictionary.ContainsKey(aliasInfo.Definition)) | ||
{ | ||
_cmdletToAliasDictionary.Add(aliasInfo.Definition, new List<String>{ aliasInfo.Name }); | ||
_cmdletToAliasDictionary.Add(aliasInfo.Definition, new List<String> { aliasInfo.Name }); | ||
} | ||
else | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
{ | ||
/// <summary> | ||
/// Utility to help handling paths across different platforms. | ||
/// </summary> | ||
/// <remarks> | ||
/// Some constants were copied from the internal System.Management.Automation.StringLiterals class. | ||
/// </remarks> | ||
internal static class PathUtils | ||
{ | ||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
internal static readonly char DefaultPathSeparator = Path.DirectorySeparatorChar; | ||
internal static readonly string DefaultPathSeparatorString = DefaultPathSeparator.ToString(); | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\'; | ||
internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString(); | ||
|
||
/// <summary> | ||
/// Converts all alternate path separators to the current platform's main path separators. | ||
/// </summary> | ||
/// <param name="path">The path to normalize.</param> | ||
/// <returns>The normalized path.</returns> | ||
public static string NormalizePathSeparators(string path) | ||
{ | ||
return string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
. ./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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
. .\ReferenceFileC.ps1 | ||
. "$PSScriptRoot\ReferenceFileC.ps1" | ||
|
||
Get-ChildItem | ||
|
||
My-Function "testb" | ||
My-Function "testb" |
Uh oh!
There was an error while loading. Please reload this page.