Skip to content
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

Merged
merged 7 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# [vNext]
* FEATURE: FindUnused Step Definitions Command - from within a binding class a new context menu command, "FindUsusedStepDefinitions", is available.
* This will list any Step Definition methods that are not matched by one or more Feature steps in the current project.

* FIX for Create Step Definition Snippets Generates Reqnroll Using Statements for SpecFlow Projects #6
* Fix for GH7 - "Find step definitions usages command not visible for SpecFlow projects
Expand Down
14 changes: 14 additions & 0 deletions Reqnroll.VisualStudio.Package/Commands/ReqnrollVsPackage.vsct
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@
<ToolTipText>Finds usages of the step definition in feature files.</ToolTipText>
</Strings>
</Button>
<Button guid="guidReqnrollPackageCmdSet" id="FindUnusedStepDefinitionsCommandId" priority="0x0103" type="Button">
<Parent guid="guidReqnrollPackageCmdSet" id="EditorContextMenuGroup" />
<Icon guid="guidImages" id="bmpReqnroll" />
<CommandFlag>DynamicVisibility</CommandFlag>
<CommandFlag>DefaultInvisible</CommandFlag>
<Strings>
<ButtonText>Find Unused Step Definitions</ButtonText>
<CommandName>Reqnroll - Find Unused Step Definitions</CommandName>
<CanonicalName>Reqnroll.FindUnusedStepDefinitions</CanonicalName>
<LocCanonicalName>Reqnroll.FindUnusedStepDefinitions</LocCanonicalName>
<ToolTipText>Finds step definitions that are not used in feature files.</ToolTipText>
</Strings>
</Button>
</Buttons>

<!--The bitmaps section is used to define the bitmaps that are used for the commands.-->
Expand Down Expand Up @@ -140,6 +153,7 @@
<IDSymbol name="FindStepDefinitionUsagesCommandId" value="0x0101" />
<IDSymbol name="RenameStepCommandId" value="0x0103" />
<IDSymbol name="GoToHookCommandId" value="0x0104" />
<IDSymbol name="FindUnusedStepDefinitionsCommandId" value="0x0102" />
</GuidSymbol>

<GuidSymbol name="guidImages" value="{027c70d2-dbf1-49ee-ba67-c4796aa26a87}" >
Expand Down
5 changes: 5 additions & 0 deletions Reqnroll.VisualStudio.Package/ProjectSystem/NullVsIdeScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ public void MonitorCommandFindStepDefinitionUsages(int usagesCount, bool isCance
{
}

public void MonitorCommandFindUnusedStepDefinitions(int unusedStepDefinitions, int scannedFeatureFiles, bool isCancellationRequested)
{
}

public void MonitorCommandGoToStepDefinition(bool generateSnippet)
{
}
Expand Down Expand Up @@ -232,5 +236,6 @@ public void MonitorWelcomeDialogDismissed(Dictionary<string, object> additionalP
public void TransmitEvent(IAnalyticsEvent runtimeEvent)
{
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public string[] GetProjectFiles(string extension)
{
return VsUtils.GetPhysicalFileProjectItems(_project)
.Select(VsUtils.GetFilePath)
.Where(fp => FileSystemHelper.IsOfType(fp, ".feature"))
.Where(fp => FileSystemHelper.IsOfType(fp, extension))
.ToArray();
}
catch (Exception e)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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);

var heuristicTest = textView.TextBuffer.CurrentSnapshot.GetText().Contains("Reqnroll") || textView.TextBuffer.CurrentSnapshot.GetText().Contains("SpecFlow");

if (status != DeveroomEditorCommandStatus.NotSupported)
// very basic heuristic: if the word "Reqnroll" or "SpecFlow" is in the content of the file, it might be a binding class
status = heuristicTest
? 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 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 = new IProjectScope[] { project };

var asyncContextMenu = IdeScope.Actions.ShowAsyncContextMenu(PopupHeader);
Task.Run(
() => FindUnusedStepDefinitionsInProjectsAsync(reqnrollTestProjects, asyncContextMenu,
asyncContextMenu.CancellationToken), asyncContextMenu.CancellationToken);
return true;
}

private async Task FindUnusedStepDefinitionsInProjectsAsync(IProjectScope[] reqnrollTestProjects, IAsyncContextMenu asyncContextMenu, CancellationToken cancellationToken)
{
var summary = new UnusedStepDefinitionSummary();
summary.ScannedProjects = reqnrollTestProjects.Length;
try
{
await FindUsagesInternalAsync(reqnrollTestProjects, 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.FoundStepDefinitions == 0)
asyncContextMenu.AddItems(
new ContextMenuItem("Could not find any step definitions in the current solution"));
else if (summary.UnusedStepDefinitions == 0)
asyncContextMenu.AddItems(
new ContextMenuItem("There are no unused step definitions"));

MonitoringService.MonitorCommandFindUnusedStepDefinitions(summary.UnusedStepDefinitions, summary.ScannedFeatureFiles,
cancellationToken.IsCancellationRequested);
if (cancellationToken.IsCancellationRequested)
Logger.LogVerbose("Finding unused step definitions cancelled");
else
Logger.LogInfo($"Found {summary.UnusedStepDefinitions} unused step definitions in {summary.ScannedProjects} Projects");
asyncContextMenu.Complete();
Finished.Set();
}
private async Task FindUsagesInternalAsync(IProjectScope[] reqnrollTestProjects, IAsyncContextMenu asyncContextMenu,

Check warning on line 100 in Reqnroll.VisualStudio/Editor/Commands/FindUnusedStepDefinitionsCommand.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
CancellationToken cancellationToken, UnusedStepDefinitionSummary summary)
{
foreach (var project in reqnrollTestProjects)
{
if (cancellationToken.IsCancellationRequested)
break;

var bindingRegistry = project.GetDiscoveryService().BindingRegistryCache.Value;
if (bindingRegistry == ProjectBindingRegistry.Invalid)
{
Logger.LogWarning(
$"Unable to get step definitions from project '{project.ProjectName}', usages will not be found for this project.");
continue;
}

// At this point, the binding registry contains StepDefinitions from the current project and any referenced assemblies that contain StepDefinitions.
// We need to filter out any step definitions that are not from the current project.

var projectCodeFiles = project.GetProjectFiles(".cs");
var projectScopedBindingRegistry = bindingRegistry.Where(sd => projectCodeFiles.Contains(sd.Implementation?.SourceLocation?.SourceFile));

var stepDefinitionCount = projectScopedBindingRegistry.StepDefinitions.Length;

summary.FoundStepDefinitions += stepDefinitionCount;
if (stepDefinitionCount == 0)
continue;

var featureFiles = project.GetProjectFiles(".feature");
var configuration = project.GetDeveroomConfiguration();
var projectUnusedStepDefinitions = _stepDefinitionsUnusedFinder.FindUnused(projectScopedBindingRegistry, featureFiles, configuration);
foreach (var unused in projectUnusedStepDefinitions)
{
if (cancellationToken.IsCancellationRequested)
break;

//await Task.Delay(500);

asyncContextMenu.AddItems(CreateMenuItem(unused, project, GetUsageLabel(unused), unused.Implementation.Method));
summary.UnusedStepDefinitions++;
}

summary.ScannedFeatureFiles += featureFiles.Length;
}
}

private static string GetUsageLabel(ProjectStepDefinitionBinding stepDefinition)
{
return $"[{stepDefinition.StepDefinitionType}(\"{stepDefinition.Expression}\")] {stepDefinition.Implementation.Method}";
}
private string GetIcon() => null;

Check warning on line 150 in Reqnroll.VisualStudio/Editor/Commands/FindUnusedStepDefinitionsCommand.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

private ContextMenuItem CreateMenuItem(ProjectStepDefinitionBinding stepDefinition, IProjectScope project, string menuLabel, string shortDescription)
{
return new SourceLocationContextMenuItem(
stepDefinition.Implementation.SourceLocation, project.ProjectFolder,
menuLabel, _ => { PerformJump<string>(shortDescription, "", stepDefinition.Implementation, _ => { } ); }, GetIcon());
}

private class UnusedStepDefinitionSummary
{
public int FoundStepDefinitions { get; set; }
public int UnusedStepDefinitions { get; set; }
public int ScannedFeatureFiles { get; set; }
public int ScannedProjects { get; set; }
public bool WasError { get; set; }

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class DeveroomCodeEditorCommandBroker : DeveroomEditorCommandBroker<IDeve
[ImportMany] IEnumerable<IDeveroomCodeEditorCommand> commands, IDeveroomLogger logger)
: base(adaptersFactory, commands, logger)
{
Debug.Assert(_commands.Count == 2, "There have to be 2 code file editor Reqnroll commands");
Debug.Assert(_commands.Count == 3, "There have to be 3 code file editor Reqnroll commands");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public static class ReqnrollVsCommands
public const int FindStepDefinitionUsagesCommandId = 0x0101;
public const int RenameStepCommandId = 0x0103;
public const int GoToHookCommandId = 0x0104;
public const int FindUnusedStepDefinitionsCommandId = 0x0102;
public static readonly Guid DefaultCommandSet = new("7fd3ed5d-2cf1-4200-b28b-cf1cc6b00c5a");
}
71 changes: 71 additions & 0 deletions Reqnroll.VisualStudio/Editor/Services/StepFinderBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Reqnroll.VisualStudio.ProjectSystem;

namespace Reqnroll.VisualStudio.Editor.Services
{
public abstract class StepFinderBase
{
private readonly IIdeScope _ideScope;

protected StepFinderBase(IIdeScope ideScope)
{
_ideScope = ideScope;
}
protected 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;
}

protected class StepFinderContext : IGherkinDocumentContext
{
public StepFinderContext(object node, IGherkinDocumentContext parent = null)

Check warning on line 61 in Reqnroll.VisualStudio/Editor/Services/StepFinderBase.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
{
Node = node;
Parent = parent;
}

public IGherkinDocumentContext Parent { get; }
public object Node { get; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 : StepFinderBase
{
private readonly IIdeScope _ideScope;

public UnusedStepDefinitionsFinder(IIdeScope ideScope) : base(ideScope)
{
_ideScope = ideScope;
}

public IEnumerable<ProjectStepDefinitionBinding> FindUnused(ProjectBindingRegistry bindingRegistry,
string[] featureFiles, DeveroomConfiguration configuration)
{
var stepDefUsageCounts = bindingRegistry.StepDefinitions.ToDictionary(stepDef => stepDef, _ => 0);
foreach (var ff in featureFiles)
{
var usedSteps = FindUsed(bindingRegistry, ff, configuration);
foreach (var step in usedSteps) stepDefUsageCounts[step]++;
}

return stepDefUsageCounts.Where(x => x.Value == 0).Select(x => x.Key);
}

protected IEnumerable<ProjectStepDefinitionBinding> FindUsed(ProjectBindingRegistry bindingRegistry,
string featureFilePath, DeveroomConfiguration configuration) =>
LoadContent(featureFilePath, out string featureFileContent)
? FindUsagesFromContent(bindingRegistry, featureFileContent, featureFilePath, configuration)
: Enumerable.Empty<ProjectStepDefinitionBinding>();

private IEnumerable<ProjectStepDefinitionBinding> FindUsagesFromContent(ProjectBindingRegistry bindingRegistry, 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 featureContext = new StepFinderContext(featureNode);
var usedSteps = new List<ProjectStepDefinitionBinding>();

foreach (var scenarioDefinition in featureNode.FlattenStepsContainers())
{
var context = new StepFinderContext(scenarioDefinition, featureContext);

foreach (var step in scenarioDefinition.Steps)
{
var matchResult = bindingRegistry.MatchStep(step, context);

usedSteps.AddRange(matchResult.Items.Where(m => m.Type == MatchResultType.Defined || m.Type == MatchResultType.Ambiguous)
.Select(i => i.MatchedStepDefinition));
}
}

return usedSteps;
}
}
1 change: 1 addition & 0 deletions Reqnroll.VisualStudio/Monitoring/IMonitoringService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public interface IMonitoringService
void MonitorCommandCommentUncomment();
void MonitorCommandDefineSteps(CreateStepDefinitionsDialogResult action, int snippetCount);
void MonitorCommandFindStepDefinitionUsages(int usagesCount, bool isCancelled);
void MonitorCommandFindUnusedStepDefinitions(int unusedStepDefinitions, int scannedFeatureFiles, bool isCancellationRequested);
void MonitorCommandGoToStepDefinition(bool generateSnippet);
void MonitorCommandGoToHook();
void MonitorCommandAutoFormatTable();
Expand Down
Loading
Loading