Skip to content
Open
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 @@ -19,6 +19,15 @@ public void ValidateParameters(ITemplateInfo template, IDictionary<string, strin
.Where(p => p.Name != "type" && p.Name != "language" && p.Name != "name")
.ToList();

// Reject parameters the template doesn't define. Silently ignoring an
// unknown --param is the most dangerous failure mode: the command "succeeds"
// but the value has no effect (e.g. a hallucinated 'FormatName').
var knownNames = new HashSet<string>(
template.ParameterDefinitions.Select(p => p.Name), StringComparer.OrdinalIgnoreCase);
var suggestionCandidates = templateParameters.Select(p => p.Name).ToList();
var templateName = template.ShortNameList?.FirstOrDefault() ?? template.Name;
errors.AddRange(FindUnknownParameters(userParameters.Keys, knownNames, suggestionCandidates, templateName));

foreach (var templateParam in templateParameters)
{
var paramName = templateParam.Name;
Expand Down Expand Up @@ -98,5 +107,71 @@ private static void ValidateParameterValue(ITemplateParameter templateParam, str
break;
}
}

/// <summary>
/// Returns an error for every user-supplied parameter key the template does not
/// define, with a best-effort "did you mean" suggestion (closest known name).
/// </summary>
public static IReadOnlyList<string> FindUnknownParameters(
IEnumerable<string> userKeys,
ISet<string> knownNames,
IReadOnlyList<string> suggestionCandidates,
string templateName)
{
var errors = new List<string>();
foreach (var key in userKeys)
{
if (knownNames.Contains(key))
{
continue;
}

var suggestion = FindClosest(key, suggestionCandidates);
var hint = suggestion is null ? string.Empty : $" Did you mean '{suggestion}'?";
errors.Add($"Unknown parameter '{key}' for template '{templateName}'.{hint}");
}
return errors;
}

/// <summary>
/// Finds the candidate closest to <paramref name="input"/> by Levenshtein distance,
/// within a small typo-sized threshold. Returns <c>null</c> when nothing is close.
/// </summary>
private static string? FindClosest(string input, IReadOnlyList<string> candidates)
{
string? best = null;
int bestDistance = int.MaxValue;
int threshold = Math.Max(2, input.Length / 3);

foreach (var candidate in candidates)
{
var distance = Levenshtein(input.ToLowerInvariant(), candidate.ToLowerInvariant());
if (distance < bestDistance)
{
bestDistance = distance;
best = candidate;
}
}

return bestDistance <= threshold ? best : null;
}

private static int Levenshtein(string a, string b)
{
var d = new int[a.Length + 1, b.Length + 1];
for (int i = 0; i <= a.Length; i++) d[i, 0] = i;
for (int j = 0; j <= b.Length; j++) d[0, j] = j;

for (int i = 1; i <= a.Length; i++)
{
for (int j = 1; j <= b.Length; j++)
{
int cost = a[i - 1] == b[j - 1] ? 0 : 1;
d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
}
}

return d[a.Length, b.Length];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using TALXIS.CLI.Features.Workspace.TemplateEngine;
using Xunit;

namespace TALXIS.CLI.Tests.TemplateEngine;

public class TemplateParameterValidatorTests
{
private static readonly string[] Known = { "TextFormat", "MaxLength", "DisplayName", "type", "language", "name" };
private static readonly string[] Suggest = { "TextFormat", "MaxLength", "DisplayName" };

private static ISet<string> KnownSet() => new HashSet<string>(Known, StringComparer.OrdinalIgnoreCase);

[Fact]
public void KnownParameter_NoError()
{
var errors = TemplateParameterValidator.FindUnknownParameters(
new[] { "TextFormat" }, KnownSet(), Suggest, "pp-entity-attribute");
Assert.Empty(errors);
}

[Fact]
public void KnownParameter_CaseInsensitive_NoError()
{
var errors = TemplateParameterValidator.FindUnknownParameters(
new[] { "textformat" }, KnownSet(), Suggest, "pp-entity-attribute");
Assert.Empty(errors);
}

[Fact]
public void SystemParameter_NoError()
{
var errors = TemplateParameterValidator.FindUnknownParameters(
new[] { "name" }, KnownSet(), Suggest, "pp-entity-attribute");
Assert.Empty(errors);
}

[Fact]
public void UnknownParameter_ProducesError_WithTemplateName()
{
var errors = TemplateParameterValidator.FindUnknownParameters(
new[] { "Xyzzy" }, KnownSet(), Suggest, "pp-entity-attribute");

var msg = Assert.Single(errors);
Assert.Contains("Unknown parameter 'Xyzzy'", msg);
Assert.Contains("pp-entity-attribute", msg);
}

[Fact]
public void UnknownParameter_Typo_SuggestsClosest()
{
var errors = TemplateParameterValidator.FindUnknownParameters(
new[] { "TextFromat" }, KnownSet(), Suggest, "pp-entity-attribute"); // 'rm' transposed

var msg = Assert.Single(errors);
Assert.Contains("Did you mean 'TextFormat'?", msg);
}

[Fact]
public void UnknownParameter_FarFromAll_NoSuggestion()
{
var errors = TemplateParameterValidator.FindUnknownParameters(
new[] { "CompletelyUnrelatedThing" }, KnownSet(), Suggest, "pp-entity-attribute");

var msg = Assert.Single(errors);
Assert.DoesNotContain("Did you mean", msg);
}
}