Skip to content

Commit 121b129

Browse files
authored
More cohost code actions tests! (#11179)
I couldn't originally test some code actions because they used the file system, so I intended this PR to fix that by abstracting the file system away. It is that, but I also found some more tests that we lacked coverage for in cohosting, and since I find the cohosting tests to be just so wonderful (not biased at all, obviously) I thought I'd be a bit extra. I deliberately didn't try to fix any of the code actions, but I think they could probably all do with some nicer whitespace handling. Managed to avoid the self-nerd-snipe this time though.
2 parents 34cacfa + 43c0969 commit 121b129

File tree

14 files changed

+534
-142
lines changed

14 files changed

+534
-142
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DirectoryHelper.cs renamed to src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IFileSystemExtensions.cs

Lines changed: 13 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
using System.Linq;
88
using Microsoft.CodeAnalysis.Razor;
99
using Microsoft.CodeAnalysis.Razor.Logging;
10+
using Microsoft.CodeAnalysis.Razor.Workspaces;
1011

1112
namespace Microsoft.AspNetCore.Razor.LanguageServer;
1213

13-
internal static class DirectoryHelper
14+
internal static class IFileSystemExtensions
1415
{
1516
/// <summary>
1617
/// Finds all the files in a directory which meet the given criteria.
@@ -23,36 +24,12 @@ internal static class DirectoryHelper
2324
/// <returns>A list of files within the given directory that meet the search criteria.</returns>
2425
/// <remarks>This method is needed to avoid problematic folders such as "node_modules" which are known not to yield the desired results or may cause performance issues.</remarks>
2526
internal static IEnumerable<string> GetFilteredFiles(
27+
this IFileSystem fileSystem,
2628
string workspaceDirectory,
2729
string searchPattern,
2830
IReadOnlyCollection<string> ignoredDirectories,
29-
#pragma warning disable CS0618 // Type or member is obsolete
30-
IFileSystem? fileSystem = null,
31-
#pragma warning restore CS0618 // Type or member is obsolete
32-
ILogger? logger = null)
31+
ILogger logger)
3332
{
34-
if (workspaceDirectory is null)
35-
{
36-
throw new ArgumentNullException(nameof(workspaceDirectory));
37-
}
38-
39-
if (searchPattern is null)
40-
{
41-
throw new ArgumentNullException(nameof(searchPattern));
42-
}
43-
44-
if (ignoredDirectories is null || ignoredDirectories.Count == 0)
45-
{
46-
throw new ArgumentNullException(nameof(ignoredDirectories));
47-
}
48-
49-
if (fileSystem is null)
50-
{
51-
#pragma warning disable CS0618 // Type or member is obsolete
52-
fileSystem = new DefaultFileSystem();
53-
#pragma warning restore CS0618 // Type or member is obsolete
54-
}
55-
5633
IEnumerable<string> files;
5734
try
5835
{
@@ -62,21 +39,21 @@ internal static IEnumerable<string> GetFilteredFiles(
6239
{
6340
// The filesystem may have deleted the directory between us finding it and searching for files in it.
6441
// This can also happen if the directory is too long for windows.
65-
files = Array.Empty<string>();
42+
files = [];
6643
}
6744
catch (UnauthorizedAccessException ex)
6845
{
69-
logger?.LogWarning($"UnauthorizedAccess: {ex.Message}");
46+
logger.LogWarning($"UnauthorizedAccess: {ex.Message}");
7047
yield break;
7148
}
7249
catch (PathTooLongException ex)
7350
{
74-
logger?.LogWarning($"PathTooLong: {ex.Message}");
51+
logger.LogWarning($"PathTooLong: {ex.Message}");
7552
yield break;
7653
}
7754
catch (IOException ex)
7855
{
79-
logger?.LogWarning($"IOException: {ex.Message}");
56+
logger.LogWarning($"IOException: {ex.Message}");
8057
yield break;
8158
}
8259

@@ -94,21 +71,21 @@ internal static IEnumerable<string> GetFilteredFiles(
9471
{
9572
// The filesystem may have deleted the directory between us finding it and searching for directories in it.
9673
// This can also happen if the directory is too long for windows.
97-
directories = Array.Empty<string>();
74+
directories = [];
9875
}
9976
catch (UnauthorizedAccessException ex)
10077
{
101-
logger?.LogWarning($"UnauthorizedAccess: {ex.Message}");
78+
logger.LogWarning($"UnauthorizedAccess: {ex.Message}");
10279
yield break;
10380
}
10481
catch (PathTooLongException ex)
10582
{
106-
logger?.LogWarning($"PathTooLong: {ex.Message}");
83+
logger.LogWarning($"PathTooLong: {ex.Message}");
10784
yield break;
10885
}
10986
catch (IOException ex)
11087
{
111-
logger?.LogWarning($"IOException: {ex.Message}");
88+
logger.LogWarning($"IOException: {ex.Message}");
11289
yield break;
11390
}
11491

@@ -117,33 +94,11 @@ internal static IEnumerable<string> GetFilteredFiles(
11794
var directory = Path.GetFileName(path);
11895
if (!ignoredDirectories.Contains(directory, FilePathComparer.Instance))
11996
{
120-
foreach (var result in GetFilteredFiles(path, searchPattern, ignoredDirectories, fileSystem, logger))
97+
foreach (var result in GetFilteredFiles(fileSystem, path, searchPattern, ignoredDirectories, logger))
12198
{
12299
yield return result;
123100
}
124101
}
125102
}
126103
}
127-
128-
[Obsolete("This only exists to enable testing, do not use it outside of tests for this class")]
129-
internal interface IFileSystem
130-
{
131-
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption);
132-
133-
public IEnumerable<string> GetDirectories(string workspaceDirectory);
134-
}
135-
136-
[Obsolete("This only exists to enable testing, do not use it outside of tests for this class")]
137-
private class DefaultFileSystem : IFileSystem
138-
{
139-
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
140-
{
141-
return Directory.GetFiles(workspaceDirectory, searchPattern, searchOption);
142-
}
143-
144-
public IEnumerable<string> GetDirectories(string workspaceDirectory)
145-
{
146-
return Directory.GetDirectories(workspaceDirectory);
147-
}
148-
}
149104
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetector.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
using Microsoft.AspNetCore.Razor.PooledObjects;
1414
using Microsoft.AspNetCore.Razor.Utilities;
1515
using Microsoft.CodeAnalysis.Razor;
16+
using Microsoft.CodeAnalysis.Razor.Logging;
17+
using Microsoft.CodeAnalysis.Razor.Workspaces;
1618
using Microsoft.VisualStudio.Threading;
1719

1820
namespace Microsoft.AspNetCore.Razor.LanguageServer;
@@ -30,13 +32,15 @@ internal partial class RazorFileChangeDetector : IFileChangeDetector, IDisposabl
3032
private readonly Dictionary<string, (RazorFileChangeKind kind, int index)> _filePathToChangeMap;
3133
private readonly HashSet<int> _indicesToSkip;
3234
private readonly List<FileSystemWatcher> _watchers;
35+
private readonly IFileSystem _fileSystem;
36+
private readonly ILogger _logger;
3337

34-
public RazorFileChangeDetector(IEnumerable<IRazorFileChangeListener> listeners)
35-
: this(listeners, s_delay)
38+
public RazorFileChangeDetector(IEnumerable<IRazorFileChangeListener> listeners, IFileSystem fileSystem, ILoggerFactory loggerFactory)
39+
: this(listeners, fileSystem, loggerFactory, s_delay)
3640
{
3741
}
3842

39-
protected RazorFileChangeDetector(IEnumerable<IRazorFileChangeListener> listeners, TimeSpan delay)
43+
protected RazorFileChangeDetector(IEnumerable<IRazorFileChangeListener> listeners, IFileSystem fileSystem, ILoggerFactory loggerFactory, TimeSpan delay)
4044
{
4145
_listeners = listeners.ToImmutableArray();
4246

@@ -45,6 +49,8 @@ protected RazorFileChangeDetector(IEnumerable<IRazorFileChangeListener> listener
4549
_filePathToChangeMap = new(FilePathComparer.Instance);
4650
_indicesToSkip = [];
4751
_watchers = new List<FileSystemWatcher>(s_razorFileExtensions.Length);
52+
_fileSystem = fileSystem;
53+
_logger = loggerFactory.GetOrCreateLogger<RazorFileChangeDetector>();
4854
}
4955

5056
public void Dispose()
@@ -218,7 +224,7 @@ protected virtual ImmutableArray<string> GetExistingRazorFiles(string workspaceD
218224

219225
foreach (var extension in s_razorFileExtensions)
220226
{
221-
var existingFiles = DirectoryHelper.GetFilteredFiles(workspaceDirectory, "*" + extension, s_ignoredDirectories);
227+
var existingFiles = _fileSystem.GetFilteredFiles(workspaceDirectory, "*" + extension, s_ignoredDirectories, _logger);
222228
result.AddRange(existingFiles);
223229
}
224230

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ protected override ILspServices ConstructLspServices()
123123
services.AddSingleton(featureOptions);
124124

125125
services.AddSingleton<IFilePathService, LSPFilePathService>();
126+
services.AddSingleton<IFileSystem, FileSystem>();
126127

127128
services.AddLifeCycleServices(this, _clientConnection, _lspServerActivationTracker);
128129

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions;
2525

2626
using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
2727

28-
internal class ComponentAccessibilityCodeActionProvider : IRazorCodeActionProvider
28+
internal class ComponentAccessibilityCodeActionProvider(IFileSystem fileSystem) : IRazorCodeActionProvider
2929
{
30+
private readonly IFileSystem _fileSystem = fileSystem;
31+
3032
public async Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
3133
{
3234
// Locate cursor
@@ -89,7 +91,7 @@ private static bool IsApplicableTag(IStartTagSyntaxNode startTag)
8991
return true;
9092
}
9193

92-
private static void AddCreateComponentFromTag(RazorCodeActionContext context, IStartTagSyntaxNode startTag, List<RazorVSInternalCodeAction> container)
94+
private void AddCreateComponentFromTag(RazorCodeActionContext context, IStartTagSyntaxNode startTag, List<RazorVSInternalCodeAction> container)
9395
{
9496
if (!context.SupportsFileCreation)
9597
{
@@ -103,7 +105,7 @@ private static void AddCreateComponentFromTag(RazorCodeActionContext context, IS
103105
Assumes.NotNull(directoryName);
104106

105107
var newComponentPath = Path.Combine(directoryName, $"{startTag.Name.Content}.razor");
106-
if (File.Exists(newComponentPath))
108+
if (_fileSystem.FileExists(newComponentPath))
107109
{
108110
return;
109111
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.CodeAnalysis.Razor.Formatting;
2020
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
2121
using Microsoft.CodeAnalysis.Razor.Protocol;
22+
using Microsoft.CodeAnalysis.Razor.Workspaces;
2223
using Microsoft.VisualStudio.LanguageServer.Protocol;
2324
using CSharpSyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
2425

@@ -27,11 +28,13 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor;
2728
internal class GenerateMethodCodeActionResolver(
2829
IRoslynCodeActionHelpers roslynCodeActionHelpers,
2930
IDocumentMappingService documentMappingService,
30-
IRazorFormattingService razorFormattingService) : IRazorCodeActionResolver
31+
IRazorFormattingService razorFormattingService,
32+
IFileSystem fileSystem) : IRazorCodeActionResolver
3133
{
3234
private readonly IRoslynCodeActionHelpers _roslynCodeActionHelpers = roslynCodeActionHelpers;
3335
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
3436
private readonly IRazorFormattingService _razorFormattingService = razorFormattingService;
37+
private readonly IFileSystem _fileSystem = fileSystem;
3538

3639
private const string ReturnType = "$$ReturnType$$";
3740
private const string MethodName = "$$MethodName$$";
@@ -58,7 +61,7 @@ internal class GenerateMethodCodeActionResolver(
5861
var razorClassName = Path.GetFileNameWithoutExtension(uriPath);
5962
var codeBehindPath = $"{uriPath}.cs";
6063

61-
if (!File.Exists(codeBehindPath) ||
64+
if (!_fileSystem.FileExists(codeBehindPath) ||
6265
razorClassName is null ||
6366
!code.TryComputeNamespace(fallbackToRootNamespace: true, out var razorNamespace))
6467
{
@@ -72,7 +75,7 @@ razorClassName is null ||
7275
cancellationToken).ConfigureAwait(false);
7376
}
7477

75-
var content = File.ReadAllText(codeBehindPath);
78+
var content = _fileSystem.ReadFile(codeBehindPath);
7679
if (GetCSharpClassDeclarationSyntax(content, razorNamespace, razorClassName) is not { } @class)
7780
{
7881
// The code behind file is malformed, generate the code in the razor file instead.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
7+
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
8+
9+
internal sealed class FileSystem : IFileSystem
10+
{
11+
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
12+
=> Directory.GetFiles(workspaceDirectory, searchPattern, searchOption);
13+
14+
public IEnumerable<string> GetDirectories(string workspaceDirectory)
15+
=> Directory.GetDirectories(workspaceDirectory);
16+
17+
public bool FileExists(string filePath)
18+
=> File.Exists(filePath);
19+
20+
public string ReadFile(string filePath)
21+
=> File.ReadAllText(filePath);
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
7+
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
8+
9+
internal interface IFileSystem
10+
{
11+
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption);
12+
13+
public IEnumerable<string> GetDirectories(string workspaceDirectory);
14+
15+
bool FileExists(string filePath);
16+
17+
string ReadFile(string filePath);
18+
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ internal sealed class OOPExtractToCodeBehindCodeActionProvider(ILoggerFactory lo
4343
internal sealed class OOPExtractToComponentCodeActionProvider : ExtractToComponentCodeActionProvider;
4444

4545
[Export(typeof(IRazorCodeActionProvider)), Shared]
46-
internal sealed class OOPComponentAccessibilityCodeActionProvider : ComponentAccessibilityCodeActionProvider;
46+
[method: ImportingConstructor]
47+
internal sealed class OOPComponentAccessibilityCodeActionProvider(IFileSystem fileSystem) : ComponentAccessibilityCodeActionProvider(fileSystem);
4748

4849
[Export(typeof(IRazorCodeActionProvider)), Shared]
4950
internal sealed class OOPGenerateMethodCodeActionProvider : GenerateMethodCodeActionProvider;
@@ -84,8 +85,9 @@ internal sealed class OOPAddUsingsCodeActionResolver : AddUsingsCodeActionResolv
8485
internal sealed class OOPGenerateMethodCodeActionResolver(
8586
IRoslynCodeActionHelpers roslynCodeActionHelpers,
8687
IDocumentMappingService documentMappingService,
87-
IRazorFormattingService razorFormattingService)
88-
: GenerateMethodCodeActionResolver(roslynCodeActionHelpers, documentMappingService, razorFormattingService);
88+
IRazorFormattingService razorFormattingService,
89+
IFileSystem fileSystem)
90+
: GenerateMethodCodeActionResolver(roslynCodeActionHelpers, documentMappingService, razorFormattingService, fileSystem);
8991

9092
[Export(typeof(ICSharpCodeActionResolver)), Shared]
9193
[method: ImportingConstructor]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Composition;
6+
using System.IO;
7+
using Microsoft.CodeAnalysis.Razor.Workspaces;
8+
9+
namespace Microsoft.CodeAnalysis.Remote.Razor;
10+
11+
[Export(typeof(IFileSystem)), Shared]
12+
internal class RemoteFileSystem : IFileSystem
13+
{
14+
private IFileSystem _fileSystem = new FileSystem();
15+
16+
public bool FileExists(string filePath)
17+
=> _fileSystem.FileExists(filePath);
18+
19+
public string ReadFile(string filePath)
20+
=> _fileSystem.ReadFile(filePath);
21+
22+
public IEnumerable<string> GetDirectories(string workspaceDirectory)
23+
=> _fileSystem.GetDirectories(workspaceDirectory);
24+
25+
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
26+
=> _fileSystem.GetFiles(workspaceDirectory, searchPattern, searchOption);
27+
28+
internal TestAccessor GetTestAccessor() => new(this);
29+
30+
internal readonly struct TestAccessor(RemoteFileSystem instance)
31+
{
32+
public void SetFileSystem(IFileSystem fileSystem)
33+
{
34+
instance._fileSystem = fileSystem;
35+
}
36+
}
37+
}

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTestBase.NetFx.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ internal GenerateMethodCodeActionResolver[] CreateRazorCodeActionResolvers(
333333
new GenerateMethodCodeActionResolver(
334334
roslynCodeActionHelpers,
335335
new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory),
336-
razorFormattingService)
336+
razorFormattingService,
337+
new FileSystem())
337338
];
338339
}

0 commit comments

Comments
 (0)