Skip to content

Commit 76626af

Browse files
committed
Fix #99: 'using module' relative path resolution
This change fixes an issue with relative module path resolution with the 'using module' statement in more recent versions of PowerShell v5. A new method overload was added to the PowerShell API's Parser.ParseInput method which allows the script's path to be specified. By using this parameter, the PowerShell parser resolves relative module paths correctly. This change also required a change to how PowerShell API usage is verified for different PowerShell versions. Since the PowerShell v5 reference assemblies don't (yet) contain Parser.ParseInput overload, we now have to make a distinction between the PowerShell v5 which is specified by the reference assemblies and the one that is currently shipped in the latest official Windows 10 release (10586). Once updated reference assemblies have been shipped for PowerShell v5 we may revisit this approach.
1 parent 9451218 commit 76626af

File tree

12 files changed

+138
-31
lines changed

12 files changed

+138
-31
lines changed

src/PowerShellEditorServices/PowerShellEditorServices.csproj

+10-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@
1313
<FileAlignment>512</FileAlignment>
1414
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
1515
<RestorePackages>true</RestorePackages>
16-
<DefineConstants Condition=" '$(PowerShellVersion)' == 'v3'">PowerShellv3</DefineConstants>
17-
<DefineConstants Condition=" '$(PowerShellVersion)' == 'v4'">PowerShellv4</DefineConstants>
18-
<DefineConstants Condition=" '$(PowerShellVersion)' == '' Or '$(PowerShellVersion)' == 'v5'">PowerShellv5</DefineConstants>
16+
<DefineConstants Condition="'$(PowerShellVersion)' == 'v3'">$(DefineConstants);PowerShellv3</DefineConstants>
17+
<DefineConstants Condition="'$(PowerShellVersion)' == 'v4'">$(DefineConstants);PowerShellv4</DefineConstants>
18+
<!-- NOTE: -->
19+
<!-- For PowerShell v5 there are some differences between the APIs released with -->
20+
<!-- Windows 10 RTM and Windows 10 Update 1, thus we need to distinguish between -->
21+
<!-- the two in our code. v5r1 indicates Win 10 RTM (10240) and v5r2 indicates -->
22+
<!-- Win10 Update 1 (10586). The PowerShellv5 constant will be defined for both -->
23+
<!-- of these versions for anything that should work against both APIs. -->
24+
<DefineConstants Condition="'$(PowerShellVersion)' == 'v5r1'">$(DefineConstants);PowerShellv5r1;PowerShellv5</DefineConstants>
25+
<DefineConstants Condition="'$(PowerShellVersion)' == 'v5r2' Or '$(PowerShellVersion)' == ''">$(DefineConstants);PowerShellv5r2;PowerShellv5</DefineConstants>
1926
</PropertyGroup>
2027
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
2128
<DebugSymbols>true</DebugSymbols>

src/PowerShellEditorServices/Session/EditorSession.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,15 @@ public class EditorSession
5858
/// </summary>
5959
public void StartSession()
6060
{
61-
// Create a workspace to contain open files
62-
this.Workspace = new Workspace();
63-
6461
// Initialize all services
6562
this.PowerShellContext = new PowerShellContext();
6663
this.LanguageService = new LanguageService(this.PowerShellContext);
6764
this.AnalysisService = new AnalysisService();
6865
this.DebugService = new DebugService(this.PowerShellContext);
6966
this.ConsoleService = new ConsoleService(this.PowerShellContext);
67+
68+
// Create a workspace to contain open files
69+
this.Workspace = new Workspace(this.PowerShellContext.PowerShellVersion);
7070
}
7171

7272
#endregion

src/PowerShellEditorServices/Workspace/ScriptFile.cs

+43-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class ScriptFile
2121
#region Private Fields
2222

2323
private Token[] scriptTokens;
24+
private Version powerShellVersion;
2425

2526
#endregion
2627

@@ -126,12 +127,18 @@ public string[] ReferencedFiles
126127
/// <param name="filePath">The path at which the script file resides.</param>
127128
/// <param name="clientFilePath">The path which the client uses to identify the file.</param>
128129
/// <param name="textReader">The TextReader to use for reading the file's contents.</param>
129-
public ScriptFile(string filePath, string clientFilePath, TextReader textReader)
130+
/// <param name="powerShellVersion">The version of PowerShell for which the script is being parsed.</param>
131+
public ScriptFile(
132+
string filePath,
133+
string clientFilePath,
134+
TextReader textReader,
135+
Version powerShellVersion)
130136
{
131137
this.FilePath = filePath;
132138
this.ClientFilePath = clientFilePath;
133139
this.IsAnalysisEnabled = true;
134140
this.IsInMemory = Workspace.IsPathInMemory(filePath);
141+
this.powerShellVersion = powerShellVersion;
135142

136143
this.SetFileContents(textReader.ReadToEnd());
137144
}
@@ -142,11 +149,17 @@ public ScriptFile(string filePath, string clientFilePath, TextReader textReader)
142149
/// <param name="filePath">The path at which the script file resides.</param>
143150
/// <param name="clientFilePath">The path which the client uses to identify the file.</param>
144151
/// <param name="initialBuffer">The initial contents of the script file.</param>
145-
public ScriptFile(string filePath, string clientFilePath, string initialBuffer)
152+
/// <param name="powerShellVersion">The version of PowerShell for which the script is being parsed.</param>
153+
public ScriptFile(
154+
string filePath,
155+
string clientFilePath,
156+
string initialBuffer,
157+
Version powerShellVersion)
146158
{
147159
this.FilePath = filePath;
148160
this.ClientFilePath = clientFilePath;
149161
this.IsAnalysisEnabled = true;
162+
this.powerShellVersion = powerShellVersion;
150163

151164
this.SetFileContents(initialBuffer);
152165
}
@@ -358,15 +371,39 @@ private void ParseFileContents()
358371

359372
try
360373
{
374+
#if PowerShellv5r2
375+
// This overload appeared with Windows 10 Update 1
376+
if (this.powerShellVersion.Major >= 5 &&
377+
this.powerShellVersion.Build >= 10586)
378+
{
379+
// Include the file path so that module relative
380+
// paths are evaluated correctly
381+
this.ScriptAst =
382+
Parser.ParseInput(
383+
this.Contents,
384+
this.FilePath,
385+
out this.scriptTokens,
386+
out parseErrors);
387+
}
388+
else
389+
{
390+
this.ScriptAst =
391+
Parser.ParseInput(
392+
this.Contents,
393+
out this.scriptTokens,
394+
out parseErrors);
395+
}
396+
#else
361397
this.ScriptAst =
362398
Parser.ParseInput(
363-
this.Contents,
364-
out this.scriptTokens,
399+
this.Contents,
400+
out this.scriptTokens,
365401
out parseErrors);
402+
#endif
366403
}
367404
catch (RuntimeException ex)
368405
{
369-
var parseError =
406+
var parseError =
370407
new ParseError(
371408
null,
372409
ex.ErrorRecord.FullyQualifiedErrorId,
@@ -388,6 +425,6 @@ private void ParseFileContents()
388425
AstOperations.FindDotSourcedIncludes(this.ScriptAst);
389426
}
390427

391-
#endregion
428+
#endregion
392429
}
393430
}

src/PowerShellEditorServices/Workspace/Workspace.cs

+28-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class Workspace
2020
{
2121
#region Private Fields
2222

23+
private Version powerShellVersion;
2324
private Dictionary<string, ScriptFile> workspaceFiles = new Dictionary<string, ScriptFile>();
2425

2526
#endregion
@@ -33,6 +34,19 @@ public class Workspace
3334

3435
#endregion
3536

37+
#region Constructors
38+
39+
/// <summary>
40+
/// Creates a new instance of the Workspace class.
41+
/// </summary>
42+
/// <param name="powerShellVersion">The version of PowerShell for which scripts will be parsed.</param>
43+
public Workspace(Version powerShellVersion)
44+
{
45+
this.powerShellVersion = powerShellVersion;
46+
}
47+
48+
#endregion
49+
3650
#region Public Methods
3751

3852
/// <summary>
@@ -63,7 +77,13 @@ public ScriptFile GetFile(string filePath)
6377

6478
using (StreamReader streamReader = new StreamReader(resolvedFilePath, Encoding.UTF8))
6579
{
66-
scriptFile = new ScriptFile(resolvedFilePath, filePath, streamReader);
80+
scriptFile =
81+
new ScriptFile(
82+
resolvedFilePath,
83+
filePath,
84+
streamReader,
85+
this.powerShellVersion);
86+
6787
this.workspaceFiles.Add(keyName, scriptFile);
6888
}
6989

@@ -92,7 +112,13 @@ public ScriptFile GetFileBuffer(string filePath, string initialBuffer)
92112
ScriptFile scriptFile = null;
93113
if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile))
94114
{
95-
scriptFile = new ScriptFile(resolvedFilePath, filePath, initialBuffer);
115+
scriptFile =
116+
new ScriptFile(
117+
resolvedFilePath,
118+
filePath,
119+
initialBuffer,
120+
this.powerShellVersion);
121+
96122
this.workspaceFiles.Add(keyName, scriptFile);
97123

98124
Logger.Write(LogLevel.Verbose, "Opened file as in-memory buffer: " + resolvedFilePath);

test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs

+15
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ await this.WaitForEvent(
6363
string.IsNullOrEmpty(diagnostics.Diagnostics[0].Message));
6464
}
6565

66+
[Fact]
67+
public async Task ServiceReturnsNoErrorsForUsingRelativeModulePaths()
68+
{
69+
// Send the 'didOpen' event
70+
await this.SendOpenFileEvent("TestFiles\\Module.psm1", false);
71+
72+
// Wait for the diagnostic event
73+
PublishDiagnosticsNotification diagnostics =
74+
await this.WaitForEvent(
75+
PublishDiagnosticsNotification.Type);
76+
77+
// Was there a syntax error?
78+
Assert.Equal(0, diagnostics.Diagnostics.Length);
79+
}
80+
6681
[Fact]
6782
public async Task ServiceCompletesFunctionName()
6883
{

test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,18 @@
8585
<ItemGroup>
8686
<None Include="App.config" />
8787
<None Include="packages.config" />
88+
<None Include="TestFiles\ChildModule\ChildModule.psm1">
89+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
90+
</None>
8891
<None Include="TestFiles\CompleteFunctionName.ps1">
8992
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
9093
</None>
9194
<None Include="TestFiles\FindReferences.ps1">
9295
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
9396
</None>
97+
<None Include="TestFiles\Module.psm1">
98+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
99+
</None>
94100
<None Include="TestFiles\MultiLineReplace.ps1">
95101
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
96102
</None>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
function Get-Stuff {
2+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
using module ".\ChildModule"

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@ public class DebugServiceTests : IDisposable
2929

3030
public DebugServiceTests()
3131
{
32-
this.workspace = new Workspace();
33-
34-
// Load the test debug file
35-
this.debugScriptFile =
36-
this.workspace.GetFile(
37-
@"..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1");
38-
3932
this.powerShellContext = new PowerShellContext();
4033
this.powerShellContext.SessionStateChanged += powerShellContext_SessionStateChanged;
4134

35+
this.workspace = new Workspace(this.powerShellContext.PowerShellVersion);
36+
4237
this.debugService = new DebugService(this.powerShellContext);
4338
this.debugService.DebuggerStopped += debugService_DebuggerStopped;
4439
this.debugService.BreakpointUpdated += debugService_BreakpointUpdated;
4540
this.runnerContext = SynchronizationContext.Current;
41+
42+
// Load the test debug file
43+
this.debugScriptFile =
44+
this.workspace.GetFile(
45+
@"..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1");
4646
}
4747

4848
void powerShellContext_SessionStateChanged(object sender, SessionStateChangedEventArgs e)

test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ public class LanguageServiceTests : IDisposable
3232

3333
public LanguageServiceTests()
3434
{
35-
this.workspace = new Workspace();
36-
3735
this.powerShellContext = new PowerShellContext();
36+
this.workspace = new Workspace(this.powerShellContext.PowerShellVersion);
3837
this.languageService = new LanguageService(this.powerShellContext);
3938
}
4039

test/PowerShellEditorServices.Test/Language/PowerShellVersionTests.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ namespace Microsoft.PowerShell.EditorServices.Test.Language
1616
public class PowerShellVersionTests
1717
{
1818
[Theory]
19-
[InlineData("3")]
20-
[InlineData("4")]
21-
[InlineData("5")]
22-
public void CompilesWithPowerShellVersion(string version)
19+
[InlineData("3", "4")]
20+
[InlineData("4", "4")]
21+
[InlineData("5", "5r1")]
22+
public void CompilesWithPowerShellVersion(string version, string versionSuffix)
2323
{
2424
var assemblyPath =
2525
Path.GetFullPath(
@@ -40,15 +40,15 @@ public void CompilesWithPowerShellVersion(string version)
4040

4141
try
4242
{
43-
Compile(projectVersion, version);
43+
Compile(projectVersion, version, versionSuffix);
4444
}
4545
finally
4646
{
4747
File.Delete(projectVersion);
4848
}
4949
}
5050

51-
private void Compile(string project, string version)
51+
private void Compile(string project, string version, string versionSuffix)
5252
{
5353
string msbuild;
5454
using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MSBuild\ToolsVersions\14.0"))

test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs

+16-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
//
55

66
using Microsoft.PowerShell.EditorServices;
7+
using System;
78
using System.IO;
89
using Xunit;
910

1011
namespace PSLanguageService.Test
1112
{
1213
public class FileChangeTests
1314
{
15+
private static readonly Version PowerShellVersion = new Version("5.0");
16+
1417
[Fact]
1518
public void CanApplySingleLineInsert()
1619
{
@@ -135,7 +138,13 @@ public void FindsDotSourcedFiles()
135138

136139
using (StringReader stringReader = new StringReader(exampleScriptContents))
137140
{
138-
ScriptFile scriptFile = new ScriptFile("DotSourceTestFile.ps1", "DotSourceTestFile.ps1", stringReader);
141+
ScriptFile scriptFile =
142+
new ScriptFile(
143+
"DotSourceTestFile.ps1",
144+
"DotSourceTestFile.ps1",
145+
stringReader,
146+
PowerShellVersion);
147+
139148
Assert.Equal(3, scriptFile.ReferencedFiles.Length);
140149
System.Console.Write("a" + scriptFile.ReferencedFiles[0]);
141150
Assert.Equal(@".\athing.ps1", scriptFile.ReferencedFiles[0]);
@@ -150,7 +159,12 @@ private void AssertFileChange(
150159
using (StringReader stringReader = new StringReader(initialString))
151160
{
152161
// Create an in-memory file from the StringReader
153-
ScriptFile fileToChange = new ScriptFile("TestFile.ps1", "TestFile.ps1", stringReader);
162+
ScriptFile fileToChange =
163+
new ScriptFile(
164+
"TestFile.ps1",
165+
"TestFile.ps1",
166+
stringReader,
167+
PowerShellVersion);
154168

155169
// Apply the FileChange and assert the resulting contents
156170
fileToChange.ApplyChange(fileChange);

0 commit comments

Comments
 (0)