Skip to content

Commit f370883

Browse files
glennsartiTylerLeonhardt
authored andcommitted
(GH-879) Add filtering for CodeLens and References (#877)
* (GH-879) Add characterisation tests for Workspace.EnumeratePSFiles Previosly there were no tests for the Workspace.EnumeratePSFiles method. This commit adds tests for EnumeratePSFiles method using a set of static fixture test files. Note that the behaviour of EnumeratePSFiles changes depending on the .Net Framework edition * (GH-879) Use globbing when enumerating workspace files Previously the Workspace.EnumeratePSFiles method could only filter files based on file extension (*.ps1, *.psm1, *.psd1). However editor settings tend to use file glob patterns, but Editor Services did not have a library that could parse them. This commit: * Updates Editor Services to use the Microsoft.Extensions.FileSystemGlobbing library * Updated the build process to include the new FileSystemGlobbing DLL * The FileSystemGlobbing library uses an abstract file system to search, not an actual System.IO.FileSystem object. So to implement the same error handling and maximum depth recursion, a WorkspaceFileSystemWrapperFactory is used to create the Directory and File objects needed for the globbing library The WorkspaceFileSystemWrapperFactory can filter on: - Maximum recursion depth - Reparse points (Note that these aren't strictly Symlinks on windows. There are many other types of filesystem items which are reparse points - File system extension - Gracefully ignores any file access errors * The EnumeratePSFiles has two method signatures. One with no arguments which uses the Workspace object's default values and another where all arguments must be specified when enumerating the files * Adds tests for the EnumeratePSFiles method to ensure that it filters on glob and recursion depth. * (GH-879) Capture the editor settings and enforce file exclusions Previously the EnumeratePSFiles method was modified to be able to use globbing patterns to filter workspace files. This commit * Modifies the LanguageServerSettings class to capture the 'files' and 'search' Settings in order to determine the correct list of glob patterns to use when searching. Currently the 'files.exclude' and 'search.exclude' are merged together to generate the list of globs and then set the Workspace settings appropriately * Uses the 'search.followSymlinks' setting to determine whether to ignore reparse points Note that the LanguageClient must be configured to send these settings during the didChangeConfiguration events otherwise it will default to include everything. * (GH-879) Do not normalise paths from EnumeratePSFiles Previously the paths emitted by `EnumeratePSFiles` were normalised to use the directory path separator appropriate for the platform. In particular on Windows the paths emitted by the Microsoft.Extensions.FileSystemGlobbing library contained both forward and backward slashes. However on inspection this is not required as all the paths are converted to URIs when communicating over LSP, so the normalisation is no longer required. This commit removes the normalisation and updates the tests to reflect the new paths.
1 parent 9a33d60 commit f370883

17 files changed

+654
-125
lines changed

PowerShellEditorServices.build.ps1

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ $script:RequiredBuildAssets = @{
5656
'publish/Serilog.Sinks.Async.dll',
5757
'publish/Serilog.Sinks.Console.dll',
5858
'publish/Serilog.Sinks.File.dll',
59+
'publish/Microsoft.Extensions.FileSystemGlobbing.dll',
5960
'Microsoft.PowerShell.EditorServices.dll',
6061
'Microsoft.PowerShell.EditorServices.pdb'
6162
)

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+30
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,36 @@ await this.RunScriptDiagnosticsAsync(
728728
this.editorSession,
729729
eventContext);
730730
}
731+
732+
// Convert the editor file glob patterns into an array for the Workspace
733+
// Both the files.exclude and search.exclude hash tables look like (glob-text, is-enabled):
734+
// "files.exclude" : {
735+
// "Makefile": true,
736+
// "*.html": true,
737+
// "build/*": true
738+
// }
739+
var excludeFilePatterns = new List<string>();
740+
if (configChangeParams.Settings.Files?.Exclude != null)
741+
{
742+
foreach(KeyValuePair<string, bool> patternEntry in configChangeParams.Settings.Files.Exclude)
743+
{
744+
if (patternEntry.Value) { excludeFilePatterns.Add(patternEntry.Key); }
745+
}
746+
}
747+
if (configChangeParams.Settings.Search?.Exclude != null)
748+
{
749+
foreach(KeyValuePair<string, bool> patternEntry in configChangeParams.Settings.Files.Exclude)
750+
{
751+
if (patternEntry.Value && !excludeFilePatterns.Contains(patternEntry.Key)) { excludeFilePatterns.Add(patternEntry.Key); }
752+
}
753+
}
754+
editorSession.Workspace.ExcludeFilesGlob = excludeFilePatterns;
755+
756+
// Convert the editor file search options to Workspace properties
757+
if (configChangeParams.Settings.Search?.FollowSymlinks != null)
758+
{
759+
editorSession.Workspace.FollowSymlinks = configChangeParams.Settings.Search.FollowSymlinks;
760+
}
731761
}
732762

733763
protected async Task HandleDefinitionRequestAsync(

src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs

+47-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.PowerShell.EditorServices.Utility;
77
using System;
88
using System.Collections;
9+
using System.Collections.Generic;
910
using System.IO;
1011
using System.Reflection;
1112
using System.Security;
@@ -331,12 +332,51 @@ public void Update(
331332
}
332333
}
333334

334-
public class LanguageServerSettingsWrapper
335-
{
336-
// NOTE: This property is capitalized as 'Powershell' because the
337-
// mode name sent from the client is written as 'powershell' and
338-
// JSON.net is using camelCasing.
335+
/// <summary>
336+
/// Additional settings from the Language Client that affect Language Server operations but
337+
/// do not exist under the 'powershell' section
338+
/// </summary>
339+
public class EditorFileSettings
340+
{
341+
/// <summary>
342+
/// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the
343+
/// the glob is in effect.
344+
/// </summary>
345+
public Dictionary<string, bool> Exclude { get; set; }
346+
}
339347

340-
public LanguageServerSettings Powershell { get; set; }
341-
}
348+
/// <summary>
349+
/// Additional settings from the Language Client that affect Language Server operations but
350+
/// do not exist under the 'powershell' section
351+
/// </summary>
352+
public class EditorSearchSettings
353+
{
354+
/// <summary>
355+
/// Exclude files globs consists of hashtable with the key as the glob and a boolean value to indicate if the
356+
/// the glob is in effect.
357+
/// </summary>
358+
public Dictionary<string, bool> Exclude { get; set; }
359+
/// <summary>
360+
/// Whether to follow symlinks when searching
361+
/// </summary>
362+
public bool FollowSymlinks { get; set; } = true;
363+
}
364+
365+
public class LanguageServerSettingsWrapper
366+
{
367+
// NOTE: This property is capitalized as 'Powershell' because the
368+
// mode name sent from the client is written as 'powershell' and
369+
// JSON.net is using camelCasing.
370+
public LanguageServerSettings Powershell { get; set; }
371+
372+
// NOTE: This property is capitalized as 'Files' because the
373+
// mode name sent from the client is written as 'files' and
374+
// JSON.net is using camelCasing.
375+
public EditorFileSettings Files { get; set; }
376+
377+
// NOTE: This property is capitalized as 'Search' because the
378+
// mode name sent from the client is written as 'search' and
379+
// JSON.net is using camelCasing.
380+
public EditorSearchSettings Search { get; set; }
342381
}
382+
}

src/PowerShellEditorServices/PowerShellEditorServices.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
2222
<PackageReference Include="Serilog.Sinks.Async" Version="1.3.0" />
2323
<PackageReference Include="System.Runtime.Extensions" Version="4.3.1" />
24+
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="2.2.0" />
2425
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
2526
</ItemGroup>
2627
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">

src/PowerShellEditorServices/Workspace/Workspace.cs

+71-118
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using System.Security;
1212
using System.Text;
1313
using System.Runtime.InteropServices;
14+
using Microsoft.Extensions.FileSystemGlobbing;
15+
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
1416

1517
namespace Microsoft.PowerShell.EditorServices
1618
{
@@ -22,11 +24,29 @@ public class Workspace
2224
{
2325
#region Private Fields
2426

25-
private static readonly string[] s_psFilePatterns = new []
27+
// List of all file extensions considered PowerShell files in the .Net Core Framework.
28+
private static readonly string[] s_psFileExtensionsCoreFramework =
2629
{
27-
"*.ps1",
28-
"*.psm1",
29-
"*.psd1"
30+
".ps1",
31+
".psm1",
32+
".psd1"
33+
};
34+
35+
// .Net Core doesn't appear to use the same three letter pattern matching rule although the docs
36+
// suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'.
37+
// ref https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netcore-2.1#System_IO_Directory_GetFiles_System_String_System_String_System_IO_EnumerationOptions_
38+
private static readonly string[] s_psFileExtensionsFullFramework =
39+
{
40+
".ps1",
41+
".psm1",
42+
".psd1",
43+
".ps1xml"
44+
};
45+
46+
// An array of globs which includes everything.
47+
private static readonly string[] s_psIncludeAllGlob = new []
48+
{
49+
"**/*"
3050
};
3151

3252
private ILogger logger;
@@ -42,6 +62,16 @@ public class Workspace
4262
/// </summary>
4363
public string WorkspacePath { get; set; }
4464

65+
/// <summary>
66+
/// Gets or sets the default list of file globs to exclude during workspace searches.
67+
/// </summary>
68+
public List<string> ExcludeFilesGlob { get; set; }
69+
70+
/// <summary>
71+
/// Gets or sets whether the workspace should follow symlinks in search operations.
72+
/// </summary>
73+
public bool FollowSymlinks { get; set; }
74+
4575
#endregion
4676

4777
#region Constructors
@@ -55,6 +85,8 @@ public Workspace(Version powerShellVersion, ILogger logger)
5585
{
5686
this.powerShellVersion = powerShellVersion;
5787
this.logger = logger;
88+
this.ExcludeFilesGlob = new List<string>();
89+
this.FollowSymlinks = true;
5890
}
5991

6092
#endregion
@@ -282,135 +314,56 @@ public string GetRelativePath(string filePath)
282314
}
283315

284316
/// <summary>
285-
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner
317+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner, using default values.
286318
/// </summary>
287-
/// <returns>An enumerator over the PowerShell files found in the workspace</returns>
319+
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
288320
public IEnumerable<string> EnumeratePSFiles()
289321
{
290-
if (WorkspacePath == null || !Directory.Exists(WorkspacePath))
291-
{
292-
return Enumerable.Empty<string>();
293-
}
294-
295-
var foundFiles = new List<string>();
296-
this.RecursivelyEnumerateFiles(WorkspacePath, ref foundFiles);
297-
return foundFiles;
322+
return EnumeratePSFiles(
323+
ExcludeFilesGlob.ToArray(),
324+
s_psIncludeAllGlob,
325+
maxDepth: 64,
326+
ignoreReparsePoints: !FollowSymlinks
327+
);
298328
}
299329

300-
#endregion
301-
302-
#region Private Methods
303-
304330
/// <summary>
305-
/// Find PowerShell files recursively down from a given directory path.
306-
/// Currently collects files in depth-first order.
307-
/// Directory.GetFiles(folderPath, pattern, SearchOption.AllDirectories) would provide this,
308-
/// but a cycle in the filesystem will cause that to enter an infinite loop.
331+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner.
309332
/// </summary>
310-
/// <param name="folderPath">The path of the current directory to find files in</param>
311-
/// <param name="foundFiles">The accumulator for files found so far.</param>
312-
/// <param name="currDepth">The current depth of the recursion from the original base directory.</param>
313-
private void RecursivelyEnumerateFiles(string folderPath, ref List<string> foundFiles, int currDepth = 0)
333+
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
334+
public IEnumerable<string> EnumeratePSFiles(
335+
string[] excludeGlobs,
336+
string[] includeGlobs,
337+
int maxDepth,
338+
bool ignoreReparsePoints
339+
)
314340
{
315-
const int recursionDepthLimit = 64;
316-
317-
// Look for any PowerShell files in the current directory
318-
foreach (string pattern in s_psFilePatterns)
319-
{
320-
string[] psFiles;
321-
try
322-
{
323-
psFiles = Directory.GetFiles(folderPath, pattern, SearchOption.TopDirectoryOnly);
324-
}
325-
catch (DirectoryNotFoundException e)
326-
{
327-
this.logger.WriteHandledException(
328-
$"Could not enumerate files in the path '{folderPath}' due to it being an invalid path",
329-
e);
330-
331-
continue;
332-
}
333-
catch (PathTooLongException e)
334-
{
335-
this.logger.WriteHandledException(
336-
$"Could not enumerate files in the path '{folderPath}' due to the path being too long",
337-
e);
338-
339-
continue;
340-
}
341-
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
342-
{
343-
this.logger.WriteHandledException(
344-
$"Could not enumerate files in the path '{folderPath}' due to the path not being accessible",
345-
e);
346-
347-
continue;
348-
}
349-
catch (Exception e)
350-
{
351-
this.logger.WriteHandledException(
352-
$"Could not enumerate files in the path '{folderPath}' due to an exception",
353-
e);
354-
355-
continue;
356-
}
357-
358-
foundFiles.AddRange(psFiles);
359-
}
360-
361-
// Prevent unbounded recursion here
362-
if (currDepth >= recursionDepthLimit)
363-
{
364-
this.logger.Write(LogLevel.Warning, $"Recursion depth limit hit for path {folderPath}");
365-
return;
366-
}
367-
368-
// Add the recursive directories to search next
369-
string[] subDirs;
370-
try
371-
{
372-
subDirs = Directory.GetDirectories(folderPath);
373-
}
374-
catch (DirectoryNotFoundException e)
375-
{
376-
this.logger.WriteHandledException(
377-
$"Could not enumerate directories in the path '{folderPath}' due to it being an invalid path",
378-
e);
379-
380-
return;
381-
}
382-
catch (PathTooLongException e)
383-
{
384-
this.logger.WriteHandledException(
385-
$"Could not enumerate directories in the path '{folderPath}' due to the path being too long",
386-
e);
387-
388-
return;
389-
}
390-
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
391-
{
392-
this.logger.WriteHandledException(
393-
$"Could not enumerate directories in the path '{folderPath}' due to the path not being accessible",
394-
e);
395-
396-
return;
397-
}
398-
catch (Exception e)
341+
if (WorkspacePath == null || !Directory.Exists(WorkspacePath))
399342
{
400-
this.logger.WriteHandledException(
401-
$"Could not enumerate directories in the path '{folderPath}' due to an exception",
402-
e);
403-
404-
return;
343+
yield break;
405344
}
406345

407-
408-
foreach (string subDir in subDirs)
346+
var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher();
347+
foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); }
348+
foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); }
349+
350+
var fsFactory = new WorkspaceFileSystemWrapperFactory(
351+
WorkspacePath,
352+
maxDepth,
353+
Utils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework,
354+
ignoreReparsePoints,
355+
logger
356+
);
357+
var fileMatchResult = matcher.Execute(fsFactory.RootDirectory);
358+
foreach (FilePatternMatch item in fileMatchResult.Files)
409359
{
410-
RecursivelyEnumerateFiles(subDir, ref foundFiles, currDepth: currDepth + 1);
360+
yield return Path.Combine(WorkspacePath, item.Path);
411361
}
412362
}
413363

364+
#endregion
365+
366+
#region Private Methods
414367
/// <summary>
415368
/// Recusrively searches through referencedFiles in scriptFiles
416369
/// and builds a Dictonary of the file references

0 commit comments

Comments
 (0)