Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.0.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using explainpowershell.models;
using ExplainPowershell.SyntaxAnalyzer.Repositories;

namespace ExplainPowershell.SyntaxAnalyzer.Tests
{
/// <summary>
/// In-memory implementation of IHelpRepository for testing
/// </summary>
public class InMemoryHelpRepository : IHelpRepository
{
private readonly Dictionary<string, HelpEntity> helpData = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Add help data for testing
/// </summary>
public void AddHelpEntity(HelpEntity entity)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));

var key = entity.CommandName ?? string.Empty;
if (!string.IsNullOrEmpty(entity.ModuleName))
{
key = $"{key} {entity.ModuleName}";
}

helpData[key] = entity;
}

/// <summary>
/// Clear all help data
/// </summary>
public void Clear()
{
helpData.Clear();
}

/// <inheritdoc/>
public HelpEntity GetHelpForCommand(string commandName)
{
if (string.IsNullOrEmpty(commandName))
{
return null;
}

// First try exact match
if (helpData.TryGetValue(commandName, out var entity))
{
return entity;
}

// If not found, try to find a command with this name in any module
var key = helpData.Keys.FirstOrDefault(k =>
k.Equals(commandName, StringComparison.OrdinalIgnoreCase) ||
k.StartsWith($"{commandName} ", StringComparison.OrdinalIgnoreCase));

return key != null ? helpData[key] : null;
}

/// <inheritdoc/>
public HelpEntity GetHelpForCommand(string commandName, string moduleName)
{
if (string.IsNullOrEmpty(commandName) || string.IsNullOrEmpty(moduleName))
{
return null;
}

var key = $"{commandName} {moduleName}";
return helpData.TryGetValue(key, out var entity) ? entity : null;
}

/// <inheritdoc/>
public List<HelpEntity> GetHelpForCommandRange(string commandName)
{
if (string.IsNullOrEmpty(commandName))
{
return new List<HelpEntity>();
}

return helpData
.Where(kvp => kvp.Key.StartsWith(commandName, StringComparison.OrdinalIgnoreCase))
.Select(kvp => kvp.Value)
.ToList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ public void Setup()
var tableClient = new TableClient(
"AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;",
"HelpData");
var helpRepository = new ExplainPowershell.SyntaxAnalyzer.Repositories.TableStorageHelpRepository(tableClient);

explainer = new(
extentText: string.Empty,
client: tableClient,
helpRepository: helpRepository,
log: mockILogger,
tokens: null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ public void Setup()
var tableClient = new TableClient(
"AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;",
"HelpData");
var helpRepository = new ExplainPowershell.SyntaxAnalyzer.Repositories.TableStorageHelpRepository(tableClient);

explainer = new(
extentText: string.Empty,
client: tableClient,
helpRepository: helpRepository,
log: mockILogger,
tokens: null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System.Linq;
using explainpowershell.models;
using NUnit.Framework;

namespace ExplainPowershell.SyntaxAnalyzer.Tests
{
[TestFixture]
public class InMemoryHelpRepositoryTests
{
private InMemoryHelpRepository repository;

[SetUp]
public void Setup()
{
repository = new InMemoryHelpRepository();
}

[Test]
public void GetHelpForCommand_WithValidCommand_ReturnsEntity()
{
// Arrange
var entity = new HelpEntity
{
CommandName = "Get-Process",
Synopsis = "Gets the processes running on the local computer.",
ModuleName = "Microsoft.PowerShell.Management"
};
repository.AddHelpEntity(entity);

// Act
var result = repository.GetHelpForCommand("get-process");

// Assert
Assert.IsNotNull(result);
Assert.AreEqual("Get-Process", result.CommandName);
}

[Test]
public void GetHelpForCommand_WithInvalidCommand_ReturnsNull()
{
// Act
var result = repository.GetHelpForCommand("NonExistentCommand");

// Assert
Assert.IsNull(result);
}

[Test]
public void GetHelpForCommand_WithModule_ReturnsCorrectEntity()
{
// Arrange
var entity1 = new HelpEntity
{
CommandName = "Test-Command",
ModuleName = "Module1",
Synopsis = "Test command from Module1"
};
var entity2 = new HelpEntity
{
CommandName = "Test-Command",
ModuleName = "Module2",
Synopsis = "Test command from Module2"
};
repository.AddHelpEntity(entity1);
repository.AddHelpEntity(entity2);

// Act
var result1 = repository.GetHelpForCommand("Test-Command", "Module1");
var result2 = repository.GetHelpForCommand("Test-Command", "Module2");

// Assert
Assert.IsNotNull(result1);
Assert.AreEqual("Module1", result1.ModuleName);
Assert.IsNotNull(result2);
Assert.AreEqual("Module2", result2.ModuleName);
}

[Test]
public void GetHelpForCommandRange_WithMatchingPrefix_ReturnsMultipleEntities()
{
// Arrange
repository.AddHelpEntity(new HelpEntity { CommandName = "Get-Process" });
repository.AddHelpEntity(new HelpEntity { CommandName = "Get-Service" });
repository.AddHelpEntity(new HelpEntity { CommandName = "Set-Service" });

// Act
var result = repository.GetHelpForCommandRange("Get-");

// Assert
Assert.AreEqual(2, result.Count);
Assert.IsTrue(result.Any(e => e.CommandName == "Get-Process"));
Assert.IsTrue(result.Any(e => e.CommandName == "Get-Service"));
}

[Test]
public void GetHelpForCommandRange_WithNoMatches_ReturnsEmptyList()
{
// Arrange
repository.AddHelpEntity(new HelpEntity { CommandName = "Get-Process" });

// Act
var result = repository.GetHelpForCommandRange("Set-");

// Assert
Assert.AreEqual(0, result.Count);
}

[Test]
public void Clear_RemovesAllEntities()
{
// Arrange
repository.AddHelpEntity(new HelpEntity { CommandName = "Get-Process" });
repository.AddHelpEntity(new HelpEntity { CommandName = "Get-Service" });

// Act
repository.Clear();
var result = repository.GetHelpForCommand("Get-Process");

// Assert
Assert.IsNull(result);
}

[Test]
public void GetHelpForCommand_IsCaseInsensitive()
{
// Arrange
var entity = new HelpEntity { CommandName = "Get-Process" };
repository.AddHelpEntity(entity);

// Act
var result1 = repository.GetHelpForCommand("GET-PROCESS");
var result2 = repository.GetHelpForCommand("get-process");
var result3 = repository.GetHelpForCommand("Get-Process");

// Assert
Assert.IsNotNull(result1);
Assert.IsNotNull(result2);
Assert.IsNotNull(result3);
Assert.AreEqual(result1.CommandName, result2.CommandName);
Assert.AreEqual(result2.CommandName, result3.CommandName);
}
}
}
22 changes: 13 additions & 9 deletions explainpowershell.analysisservice/AstVisitorExplainer_classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ public override AstVisitAction VisitFunctionMember(FunctionMemberAst functionMem

var howManyParameters = functionMemberAst.Parameters.Count == 0 ? string.Empty : $"has {functionMemberAst.Parameters.Count} parameters and ";

description = $"A constructor, a special method, used to set things up within the object. Constructors have the same name as the class. This constructor {howManyParameters}is called when [{(functionMemberAst.Parent as TypeDefinitionAst).Name}]::new({parameterSignature}) is used.";
helpResult.DocumentationLink += "#constructor";
description = $"A constructor, a special method, used to set things up within the object. Constructors have the same name as the class. This constructor {howManyParameters}is called when [{(functionMemberAst.Parent as TypeDefinitionAst)?.Name ?? "Unknown"}]::new({parameterSignature}) is used.";
helpResult?.DocumentationLink += "#constructor";
}
else
{
helpResult.DocumentationLink += "#class-methods";
helpResult?.DocumentationLink += "#class-methods";
var modifier = "M";
modifier = functionMemberAst.IsHidden ? "A hidden m" : modifier;
modifier = functionMemberAst.IsStatic ? "A static m" : modifier;
Expand All @@ -119,23 +119,27 @@ public override AstVisitAction VisitFunctionMember(FunctionMemberAst functionMem

public override AstVisitAction VisitPropertyMember(PropertyMemberAst propertyMemberAst)
{
HelpEntity helpResult = null;
HelpEntity? helpResult = null;
var description = "";

if ((propertyMemberAst.Parent as TypeDefinitionAst).IsClass)
var parentType = propertyMemberAst.Parent as TypeDefinitionAst;
if (parentType?.IsClass == true)
{
var attributes = propertyMemberAst.Attributes.Count >= 0 ?
$", with attributes '{string.Join(", ", propertyMemberAst.Attributes.Select(p => p.TypeName.Name))}'." :
".";
description = $"Property '{propertyMemberAst.Name}' of type '{propertyMemberAst.PropertyType.TypeName.FullName}'{attributes}";
helpResult = HelpTableQuery("about_classes");
helpResult.DocumentationLink += "#class-properties";
helpResult = HelpTableQuery(Constants.AboutTopics.AboutClasses);
if (helpResult != null)
{
helpResult.DocumentationLink += "#class-properties";
}
}

if ((propertyMemberAst.Parent as TypeDefinitionAst).IsEnum)
if (parentType?.IsEnum == true)
{
description = $"Enum label '{propertyMemberAst.Name}', with value '{propertyMemberAst.InitialValue}'.";
helpResult = HelpTableQuery("about_enum");
helpResult = HelpTableQuery(Constants.AboutTopics.AboutEnum);
}

explanations.Add(new Explanation()
Expand Down
14 changes: 7 additions & 7 deletions explainpowershell.analysisservice/AstVisitorExplainer_command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ public override AstVisitAction VisitCommand(CommandAst commandAst)

string resolvedCmd = Helpers.ResolveAlias(cmdName) ?? cmdName;

HelpEntity helpResult;
HelpEntity? helpResult;
if (string.IsNullOrEmpty(moduleName))
{
var helpResults = HelpTableQueryRange(resolvedCmd);
helpResult = helpResults?.FirstOrDefault();
if (helpResults.Count > 1)
if (helpResults?.Count > 1)
{
this.errorMessage = $"The command '{helpResult?.CommandName}' is present in more than one module: '{string.Join("', '", helpResults.Select(r => r.ModuleName))}'. Explicitly prepend the module name to the command to select one: '{helpResults.First().ModuleName}\\{helpResult?.CommandName}'";
}
Expand Down Expand Up @@ -131,12 +131,12 @@ public override AstVisitAction VisitCommandParameter(CommandParameterAst command

var parentCommandExplanation = explanations.FirstOrDefault(e => e.Id == exp.ParentId);

ParameterData matchedParameter;
if (parentCommandExplanation.HelpResult?.Parameters != null)
ParameterData? matchedParameter = null;
if (parentCommandExplanation?.HelpResult?.Parameters != null)
{
try
{
matchedParameter = Helpers.MatchParam(commandParameterAst.ParameterName, parentCommandExplanation.HelpResult?.Parameters);
matchedParameter = Helpers.MatchParam(commandParameterAst.ParameterName, parentCommandExplanation.HelpResult.Parameters);

if (matchedParameter != null)
{
Expand Down Expand Up @@ -180,7 +180,7 @@ public override AstVisitAction VisitCommandParameter(CommandParameterAst command
.Append("__AllParameterSets")
.ToArray();

var paramSetData = Helpers.GetParameterSetData(matchedParameter, availableParamSets);
var paramSetData = Helpers.GetParameterSetData(matchedParameter, availableParamSets ?? Array.Empty<string>());

if (paramSetData.Count > 1)
{
Expand All @@ -191,7 +191,7 @@ public override AstVisitAction VisitCommandParameter(CommandParameterAst command
var paramSetName = paramSetData.Select(p => p.ParameterSetName).FirstOrDefault();
if (paramSetName == "__AllParameterSets")
{
if (availableParamSets.Length > 1)
if (availableParamSets?.Length > 1)
{
exp.Description += $"\nThis parameter is present in all parameter sets.";
}
Expand Down
Loading