-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Find Unused Step Definitions command (#8)
* Initial Commit of FEAT FindUnusedStepDefinitionsCommand. This commit duplicates code from FindStepDefinitionUsageCommand. Not fully tested, but basically working. * Added testing via Specs. Renamed StepDefinitionsUnusedFinder to UnusedStepDefinitionsFinder.cs * Cleanup per review comments. * Fixed bug in UnusedStepDefFinder that was counting as unused any StepDef in the project that didn't appear in the FF currently being examined. Minor modification to the logging to report back the number of Unused and the number of Projects (rather than the number of Features). * Restrict analysis and results to ONLY the current Project open in the editor. * Restricted FindUnusedStepDefinitions to find only those within the current project (the project of the current editor). Added same modification as in GH#7 so that this command works within SpecFlow projects. Added CHANGELOG entry for this new feature.
- Loading branch information
1 parent
9f95c1e
commit 6d5f803
Showing
14 changed files
with
521 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 169 additions & 0 deletions
169
Reqnroll.VisualStudio/Editor/Commands/FindUnusedStepDefinitionsCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / build
|
||
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; | ||
|
||
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; } | ||
|
||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
{ | ||
Node = node; | ||
Parent = parent; | ||
} | ||
|
||
public IGherkinDocumentContext Parent { get; } | ||
public object Node { get; } | ||
} | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
Reqnroll.VisualStudio/Editor/Services/UnusedStepDefinitionsFinder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.