-
Notifications
You must be signed in to change notification settings - Fork 9
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
Find Unused Step Definitions command #8
Changes from 2 commits
54bfb4f
ba72e63
407f19c
73836f0
870852e
021e06d
29ddb35
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 |
---|---|---|
@@ -0,0 +1,206 @@ | ||
namespace Reqnroll.VisualStudio.Editor.Commands | ||
{ | ||
[Export(typeof(IDeveroomCodeEditorCommand))] | ||
|
||
public class FindUnusedStepDefinitionsCommand : DeveroomEditorCommandBase, IDeveroomCodeEditorCommand | ||
{ | ||
private const string PopupHeader = "Unused Step Definitions"; | ||
|
||
private readonly UnusedStepDefinitionsFinder _stepDefinitionsUnusedFinder; | ||
|
||
[ImportingConstructor] | ||
public FindUnusedStepDefinitionsCommand( | ||
IIdeScope ideScope, | ||
IBufferTagAggregatorFactoryService aggregatorFactory, | ||
IDeveroomTaggerProvider taggerProvider) | ||
: base(ideScope, aggregatorFactory, taggerProvider) | ||
{ | ||
_stepDefinitionsUnusedFinder = new UnusedStepDefinitionsFinder(ideScope); | ||
} | ||
|
||
|
||
public override DeveroomEditorCommandTargetKey[] Targets => new[] | ||
{ | ||
new DeveroomEditorCommandTargetKey(ReqnrollVsCommands.DefaultCommandSet, | ||
ReqnrollVsCommands.FindUnusedStepDefinitionsCommandId) | ||
}; | ||
|
||
public override DeveroomEditorCommandStatus QueryStatus(IWpfTextView textView, | ||
DeveroomEditorCommandTargetKey commandKey) | ||
{ | ||
var status = base.QueryStatus(textView, commandKey); | ||
|
||
if (status != DeveroomEditorCommandStatus.NotSupported) | ||
// very basic heuristic: if the word "Reqnroll" is in the content of the file, it might be a binding class | ||
status = textView.TextBuffer.CurrentSnapshot.GetText().Contains("Reqnroll") | ||
? DeveroomEditorCommandStatus.Supported | ||
: DeveroomEditorCommandStatus.NotSupported; | ||
|
||
return status; | ||
} | ||
|
||
|
||
public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetKey commandKey, IntPtr inArgs = default) | ||
{ | ||
Logger.LogVerbose("Find Unused Step Definitions"); | ||
|
||
var textBuffer = textView.TextBuffer; | ||
var fileName = GetEditorDocumentPath(textBuffer); | ||
var triggerPoint = textView.Caret.Position.BufferPosition; | ||
|
||
var project = IdeScope.GetProject(textBuffer); | ||
if (project == null || !project.GetProjectSettings().IsReqnrollProject) | ||
{ | ||
IdeScope.Actions.ShowProblem( | ||
"Unable to find step definition usages: the project is not detected to be a Reqnroll project or it is not initialized yet."); | ||
return true; | ||
} | ||
|
||
var reqnrollTestProjects = IdeScope.GetProjectsWithFeatureFiles() | ||
.Where(p => p.GetProjectSettings().IsReqnrollTestProject) | ||
.ToArray(); | ||
|
||
if (reqnrollTestProjects.Length == 0) | ||
{ | ||
IdeScope.Actions.ShowProblem( | ||
"Unable to find step definition usages: could not find any Reqnroll project with feature files."); | ||
return true; | ||
} | ||
|
||
var asyncContextMenu = IdeScope.Actions.ShowAsyncContextMenu(PopupHeader); | ||
Task.Run( | ||
() => FindUnusedStepDefinitionsInProjectsAsync(reqnrollTestProjects, fileName, triggerPoint, asyncContextMenu, | ||
asyncContextMenu.CancellationToken), asyncContextMenu.CancellationToken); | ||
return true; | ||
} | ||
|
||
private async Task FindUnusedStepDefinitionsInProjectsAsync(IProjectScope[] reqnrollTestProjects, string fileName, | ||
SnapshotPoint triggerPoint, IAsyncContextMenu asyncContextMenu, CancellationToken cancellationToken) | ||
{ | ||
var summary = new UnusedStepDefinitionSummary(); | ||
|
||
try | ||
{ | ||
await FindUsagesInternalAsync(reqnrollTestProjects, fileName, triggerPoint, asyncContextMenu, | ||
cancellationToken, summary); | ||
} | ||
catch (Exception ex) | ||
{ | ||
Logger.LogException(MonitoringService, ex); | ||
summary.WasError = true; | ||
} | ||
|
||
if (summary.WasError) | ||
asyncContextMenu.AddItems(new ContextMenuItem("Could not complete find operation because of an error")); | ||
else if (summary.UnusedStepDefinitions == 0) | ||
asyncContextMenu.AddItems( | ||
new ContextMenuItem("Could not find any unused step definitions at the current position")); | ||
//TODO: determine if this required/needed | ||
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. I would include the not found message (with a different text of course) 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. Done. |
||
//else if (summary.UsagesFound == 0) asyncContextMenu.AddItems(new ContextMenuItem("Could not find any usage")); | ||
|
||
//TODO: modify monitoring to include finding unused step definitions | ||
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. I would definitely include the monitoring, so that we can see how users will use this feature. You can make a new telemetry event similar to the find usages. 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. Done. Added MonitorCommandFindUnusedSStepDefinitions to IMonitoringService and its two implementations (MonitoringService and NullVsIdeScope.cs) |
||
//MonitoringService.MonitorCommandFindStepDefinitionUsages(summary.UsagesFound, | ||
// cancellationToken.IsCancellationRequested); | ||
if (cancellationToken.IsCancellationRequested) | ||
Logger.LogVerbose("Finding unused step definitions cancelled"); | ||
else | ||
Logger.LogInfo($"Found {summary.UnusedStepDefinitions} unused step definitions in {summary.ScannedFeatureFiles} feature files"); | ||
asyncContextMenu.Complete(); | ||
Finished.Set(); | ||
} | ||
private async Task FindUsagesInternalAsync(IProjectScope[] reqnrollTestProjects, string fileName, | ||
SnapshotPoint triggerPoint, IAsyncContextMenu asyncContextMenu, CancellationToken cancellationToken, | ||
UnusedStepDefinitionSummary summary) | ||
{ | ||
foreach (var project in reqnrollTestProjects) | ||
{ | ||
if (cancellationToken.IsCancellationRequested) | ||
break; | ||
|
||
var stepDefinitions = await GetStepDefinitionsAsync(project, fileName, triggerPoint); | ||
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. We do not need to collect step definitions per file, because we can use the step definitions for the project (see comment below). So this few lines and the related methods ( 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. Refactored per your suggestion. |
||
//summary.FoundStepDefinitions += stepDefinitions.Length; | ||
if (stepDefinitions.Length == 0) | ||
continue; | ||
|
||
var featureFiles = project.GetProjectFiles(".feature"); | ||
var configuration = project.GetDeveroomConfiguration(); | ||
var projectUnusedStepDefinitions = _stepDefinitionsUnusedFinder.FindUnused(stepDefinitions, featureFiles, configuration); | ||
foreach (var unused in projectUnusedStepDefinitions) | ||
{ | ||
if (cancellationToken.IsCancellationRequested) | ||
break; | ||
|
||
//await Task.Delay(500); | ||
|
||
//TODO: create menu item - what type of menu item? | ||
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. 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. Done. |
||
asyncContextMenu.AddItems(CreateMenuItem(unused, project)); | ||
summary.UnusedStepDefinitions++; | ||
} | ||
|
||
summary.ScannedFeatureFiles += featureFiles.Length; | ||
} | ||
} | ||
|
||
private async Task<ProjectStepDefinitionBinding[]> GetStepDefinitionsAsync(IProjectScope project, string fileName, | ||
SnapshotPoint triggerPoint) | ||
{ | ||
var discoveryService = project.GetDiscoveryService(); | ||
var bindingRegistry = await discoveryService.BindingRegistryCache.GetLatest(); | ||
if (bindingRegistry == ProjectBindingRegistry.Invalid) | ||
Logger.LogWarning( | ||
$"Unable to get step definitions from project '{project.ProjectName}', usages will not be found for this project."); | ||
return GetStepDefinitions(fileName, triggerPoint, bindingRegistry); | ||
} | ||
|
||
//TODO: since this Command doesn't need to filter the set of StepDefinitions, this can be merged with the method above | ||
internal static ProjectStepDefinitionBinding[] GetStepDefinitions(string fileName, SnapshotPoint triggerPoint, | ||
ProjectBindingRegistry bindingRegistry) | ||
{ | ||
return bindingRegistry.StepDefinitions | ||
// .Where(sd => sd.Implementation?.SourceLocation != null && | ||
// sd.Implementation.SourceLocation.SourceFile == fileName && | ||
// | ||
.ToArray(); | ||
} | ||
|
||
//TODO: near duplicate of similar method in FindStepDefinitionUsagesCommand | ||
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. I agree. Maybe try to create a 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. After looking at it further, decided against it for now. The other is dealing with creating a menu item and Jump for feature files, not C# files. I found that the base class for these Editor Commands already has an implementation for PerformJump for C# code. So I refactored the FindUnusedStepDefinitionsCommand to use the base class behavior. The implementation that I was previously looking at, in FindStepDefinitionUsages, shadows the base class implementation because jumping to a feature file location is somewhat different. Let me know if you'd like me to further refactor this such that both classes use a common implementation. 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. Makes sense. All good. |
||
private ContextMenuItem CreateMenuItem(ProjectStepDefinitionBinding stepDefinition, IProjectScope project) | ||
{ | ||
return new SourceLocationContextMenuItem( | ||
stepDefinition.Implementation.SourceLocation, project.ProjectFolder, | ||
GetUsageLabel(stepDefinition), _ => { PerformJump(stepDefinition); }, null); | ||
} | ||
|
||
private static string GetUsageLabel(ProjectStepDefinitionBinding stepDefinition) | ||
{ | ||
return $"[{stepDefinition.StepDefinitionType}(\"{stepDefinition.Expression}\")] {stepDefinition.Implementation.Method}"; | ||
} | ||
|
||
//TODO: near duplicate of similar method in FindStepDefinitionUsagesCommand | ||
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. Like above, maybe an 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. See above |
||
private void PerformJump(ProjectStepDefinitionBinding binding) | ||
{ | ||
var sourceLocation = binding.Implementation.SourceLocation; | ||
if (sourceLocation == null) | ||
{ | ||
Logger.LogWarning($"Cannot jump to {binding.Implementation.Method}: no source location"); | ||
IdeScope.Actions.ShowProblem("Unable to jump to the step. No source location detected."); | ||
return; | ||
} | ||
|
||
Logger.LogInfo($"Jumping to {binding.Implementation.Method} at {sourceLocation}"); | ||
if (!IdeScope.Actions.NavigateTo(sourceLocation)) | ||
{ | ||
Logger.LogWarning($"Cannot jump to {binding.Implementation.Method}: invalid source file or position"); | ||
IdeScope.Actions.ShowProblem( | ||
$"Unable to jump to the step. Invalid source file or file position.{Environment.NewLine}{sourceLocation}"); | ||
} | ||
} | ||
private class UnusedStepDefinitionSummary | ||
{ | ||
public int UnusedStepDefinitions { get; set; } | ||
public int ScannedFeatureFiles { get; set; } | ||
public bool WasError { get; set; } | ||
|
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
using Reqnroll.VisualStudio.ProjectSystem; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
#nullable disable | ||
namespace Reqnroll.VisualStudio.Editor.Services; | ||
|
||
public class UnusedStepDefinitionsFinder | ||
{ | ||
private readonly IIdeScope _ideScope; | ||
|
||
public UnusedStepDefinitionsFinder(IIdeScope ideScope) | ||
{ | ||
_ideScope = ideScope; | ||
} | ||
|
||
public IEnumerable<ProjectStepDefinitionBinding> FindUnused(ProjectStepDefinitionBinding[] stepDefinitions, | ||
string[] featureFiles, DeveroomConfiguration configuration) | ||
{ | ||
return featureFiles.SelectMany(ff => FindUnused(stepDefinitions, ff, configuration)); | ||
} | ||
|
||
public IEnumerable<ProjectStepDefinitionBinding> FindUnused(ProjectStepDefinitionBinding[] stepDefinitions, | ||
string featureFilePath, DeveroomConfiguration configuration) => | ||
LoadContent(featureFilePath, out string featureFileContent) | ||
? FindUsagesFromContent(stepDefinitions, featureFileContent, featureFilePath, configuration) | ||
: Enumerable.Empty<ProjectStepDefinitionBinding>(); | ||
|
||
private IEnumerable<ProjectStepDefinitionBinding> FindUsagesFromContent(ProjectStepDefinitionBinding[] stepDefinitions, string featureFileContent, string featureFilePath, DeveroomConfiguration configuration) | ||
{ | ||
var dialectProvider = ReqnrollGherkinDialectProvider.Get(configuration.DefaultFeatureLanguage); | ||
var parser = new DeveroomGherkinParser(dialectProvider, _ideScope.MonitoringService); | ||
parser.ParseAndCollectErrors(featureFileContent, _ideScope.Logger, | ||
out var gherkinDocument, out _); | ||
|
||
var featureNode = gherkinDocument?.Feature; | ||
if (featureNode == null) | ||
return Enumerable.Empty<ProjectStepDefinitionBinding>(); | ||
|
||
var dummyRegistry = ProjectBindingRegistry.FromBindings(stepDefinitions); | ||
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. Instead of this you should get an instance of the In the finder class, you can get the binding registry with
this way you will not need the |
||
|
||
//this set keeps track of which step definitions have not been used. We start with ALL step definitions, and will subtract those that match steps in the feature file | ||
HashSet<ProjectStepDefinitionBinding> stepDefinitionsSet = new(stepDefinitions); | ||
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. Following the comment above, here you can use the |
||
|
||
var featureContext = new UnusedFinderContext(featureNode); | ||
|
||
foreach (var scenarioDefinition in featureNode.FlattenStepsContainers()) | ||
{ | ||
var context = new UnusedFinderContext(scenarioDefinition, featureContext); | ||
|
||
foreach (var step in scenarioDefinition.Steps) | ||
{ | ||
var matchResult = dummyRegistry.MatchStep(step, context); | ||
|
||
var usedSteps = matchResult.Items.Where(m => m.Type == MatchResultType.Defined || m.Type == MatchResultType.Ambiguous) | ||
.Select(i => i.MatchedStepDefinition) | ||
.ToHashSet<ProjectStepDefinitionBinding>(); | ||
stepDefinitionsSet.ExceptWith(usedSteps); | ||
} | ||
} | ||
|
||
return stepDefinitionsSet.ToList(); | ||
|
||
} | ||
|
||
//TODO: duplicate of what is in StepDefinitionUsageFinder; consider refactoring to shared implementation | ||
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. Agree. Maybe a 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. Created a base class for both Finder services, called it StepFinderBase.cs. IT contains the common logic for loading feature file content. |
||
private bool LoadContent(string featureFilePath, out string content) | ||
{ | ||
if (LoadAlreadyOpenedContent(featureFilePath, out string openedContent)) | ||
{ | ||
content = openedContent; | ||
return true; | ||
} | ||
|
||
if (LoadContentFromFile(featureFilePath, out string fileContent)) | ||
{ | ||
content = fileContent; | ||
return true; | ||
} | ||
|
||
content = string.Empty; | ||
return false; | ||
} | ||
|
||
private bool LoadContentFromFile(string featureFilePath, out string content) | ||
{ | ||
try | ||
{ | ||
content = _ideScope.FileSystem.File.ReadAllText(featureFilePath); | ||
return true; | ||
} | ||
catch (Exception ex) | ||
{ | ||
_ideScope.Logger.LogDebugException(ex); | ||
content = string.Empty; | ||
return false; | ||
} | ||
} | ||
|
||
private bool LoadAlreadyOpenedContent(string featureFilePath, out string content) | ||
{ | ||
var sl = new SourceLocation(featureFilePath, 1, 1); | ||
if (!_ideScope.GetTextBuffer(sl, out ITextBuffer tb)) | ||
{ | ||
content = string.Empty; | ||
return false; | ||
} | ||
|
||
content = tb.CurrentSnapshot.GetText(); | ||
return true; | ||
} | ||
|
||
//TODO: duplicate of what is in StepDefinitionUsageFinder; consider refactoring to shared implementation | ||
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. Agree, see comment above. 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. Created StepFinderContext class, embedded in the StepFinderBase.cs base class file. |
||
private class UnusedFinderContext : IGherkinDocumentContext | ||
{ | ||
public UnusedFinderContext(object node, IGherkinDocumentContext parent = null) | ||
{ | ||
Node = node; | ||
Parent = parent; | ||
} | ||
|
||
public IGherkinDocumentContext Parent { get; } | ||
public object Node { get; } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the message should be "Could not find any unused step definitions in the current project"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re: the messaging when there are no Unbound StepDefs. This feature scans all the projects in the solution, including external binding assemblies. At this point in the execution, all the work is done (all projects and assemblies scanned).
I would suggest:
"There are no unused step definitions" or perhaps "There are no unused step definitions in the solution"
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly. It takes the binding registry from the current project. It scans all projects for feature files, but not for the step definitions. At least I think it is like that.