|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.TemplateFiltering;
using Microsoft.TemplateEngine.Edge;
using Microsoft.TemplateEngine.Utils;
namespace Microsoft.TemplateEngine.Cli.TemplateResolution
{
/// <summary>
/// Represents template group matched to command input done by the user.
/// The template group is filtered by three groups of filters:
/// - group-based filters (short name/name and language at the moment)
/// - template info filters (type, baseline, tags)
/// - template option filters based on parameters defined in template.json.
/// The filters are executed in order above. If the filter provides no matched, the next groups of filters won't be executed.
/// </summary>
internal class TemplateGroupMatchInfo
{
private readonly TemplateGroup _templateGroup;
private readonly IReadOnlyList<MatchInfo> _groupDispositions;
private readonly IReadOnlyList<ITemplateMatchInfo> _templateMatchInfos;
private bool _isTemplateMatchEvaluated;
private bool _isTemplateParameterMatchEvaluated;
private TemplateGroupMatchInfo(
TemplateGroup templateGroup,
IEnumerable<MatchInfo> groupDispositions,
IEnumerable<ITemplateMatchInfo> templateDispositions,
bool templateMatchEvaluated = true,
bool templateParameterMatchEvaluated = true)
{
_templateGroup = templateGroup;
_groupDispositions = groupDispositions.ToList();
_templateMatchInfos = templateDispositions.ToList();
_isTemplateMatchEvaluated = templateMatchEvaluated;
_isTemplateParameterMatchEvaluated = templateParameterMatchEvaluated;
}
/// <summary>
/// Returns information about the template group (without match information).
/// </summary>
internal TemplateGroup GroupInfo => _templateGroup;
/// <summary>
/// True when the group matches group filters.
/// </summary>
internal bool IsGroupMatch => _groupDispositions.All(x => x.Kind != MatchKind.Mismatch);
/// <summary>
/// True when the group matches group and template info filters.
/// </summary>
internal bool IsGroupAndTemplateInfoMatch => IsGroupMatch && IsGroupTemplateInfoMatch;
/// <summary>
/// True when the group matches group, template info, and template options filters.
/// </summary>
internal bool IsGroupAndTemplateInfoAndParametersMatch => IsGroupMatch && IsGroupTemplateInfoMatch && IsGroupTemplateParametersMatch;
/// <summary>
/// Returns the list of <see cref="MatchInfo"/> for the group-based filters.
/// </summary>
internal IReadOnlyList<MatchInfo> GroupMatchInfos
{
get
{
return _groupDispositions;
}
}
/// <summary>
/// Returns the list of <see cref="ITemplateMatchInfo"></see> for all templates in the group.
/// </summary>
internal IEnumerable<ITemplateMatchInfo> TemplateMatchInfos
{
get
{
if (!_isTemplateMatchEvaluated)
{
throw new NotSupportedException("Template info matches were not evaluated.");
}
if (!IsGroupMatch)
{
return Array.Empty<ITemplateMatchInfo>();
}
return _templateMatchInfos;
}
}
/// <summary>
/// Returns the list of <see cref="ITemplateMatchInfo"></see> for all templates that matches template info filters.
/// </summary>
internal IEnumerable<ITemplateMatchInfo> TemplateMatchInfosWithMatchingInfo
{
get
{
if (!_isTemplateMatchEvaluated)
{
throw new NotSupportedException("Template info matches were not evaluated.");
}
if (!IsGroupMatch)
{
return Array.Empty<ITemplateMatchInfo>();
}
return _templateMatchInfos.Where(template => TemplateInfoMatch(template));
}
}
/// <summary>
/// Returns the list of <see cref="ITemplateMatchInfo"></see> for all templates that matches template info filters and template options filters.
/// </summary>
internal IEnumerable<ITemplateMatchInfo> TemplateMatchInfosWithMatchingParameters
{
get
{
if (!_isTemplateMatchEvaluated)
{
throw new NotSupportedException("Template info matches were not evaluated.");
}
if (!_isTemplateParameterMatchEvaluated)
{
throw new NotSupportedException("Template parameters matches were not evaluated.");
}
if (!IsGroupMatch)
{
return Array.Empty<ITemplateMatchInfo>();
}
return _templateMatchInfos.Where(template => TemplateInfoMatch(template) && TemplateParametersMatch(template));
}
}
/// <summary>
/// Returns the list of <see cref="ITemplateMatchInfo"></see> for all templates of preferred language that matches template info filters, template options filters.
/// </summary>
internal IEnumerable<ITemplateMatchInfo> TemplateMatchInfosWithMatchingParametersForPreferredLanguage
{
get
{
if (!_isTemplateMatchEvaluated)
{
throw new NotSupportedException("Template info matches were not evaluated.");
}
if (!_isTemplateParameterMatchEvaluated)
{
throw new NotSupportedException("Template parameters matches were not evaluated.");
}
if (!IsGroupMatch)
{
return Array.Empty<ITemplateMatchInfo>();
}
return FilterTemplatesByPreferredLanguage(_templateMatchInfos.Where(template => TemplateInfoMatch(template) && TemplateParametersMatch(template)));
}
}
/// <summary>
/// Returns the list of <see cref="ITemplateInfo"></see> for all templates that matches template info filters.
/// Same as <see cref="TemplateMatchInfosWithMatchingInfo"/>, but returns <see cref="ITemplateInfo"/> without match information.
/// </summary>
internal IEnumerable<ITemplateInfo> TemplatesWithMatchingInfo => TemplateMatchInfosWithMatchingInfo.Select(template => template.Info);
/// <summary>
/// Returns the list of <see cref="ITemplateInfo"></see> for all templates that matches template info filters and template options filters.
/// Same as <see cref="TemplateMatchInfosWithMatchingParameters"/>, but returns <see cref="ITemplateInfo"/> without match information.
/// </summary>
internal IEnumerable<ITemplateInfo> TemplatesWithMatchingParameters => TemplateMatchInfosWithMatchingParameters.Select(template => template.Info);
/// <summary>
/// Returns the list of <see cref="ITemplateInfo"></see> for all templates of preferred language that matches template info filters, template options filters.
/// Same as <see cref="TemplateMatchInfosWithMatchingParametersForPreferredLanguage"/>, but returns <see cref="ITemplateInfo"/> without match information.
/// </summary>
internal IEnumerable<ITemplateInfo> TemplatesWithMatchingParametersForPreferredLanguage
=> TemplateMatchInfosWithMatchingParametersForPreferredLanguage.Select(template => template.Info);
private bool IsGroupTemplateInfoMatch => _isTemplateMatchEvaluated && _templateMatchInfos.Any(template => TemplateInfoMatch(template));
private bool IsGroupTemplateParametersMatch => _isTemplateParameterMatchEvaluated && _templateMatchInfos.Any(template => TemplateParametersMatch(template));
/// <summary>
/// Applies filters to template group.
/// </summary>
/// <param name="group">The template group to apply filters to.</param>
/// <param name="groupFilters">The group-based filters.</param>
/// <param name="templateInfoFilters">The template info filters.</param>
/// <param name="templateParametersFilter">The template parameters filters.</param>
/// <returns><see cref="TemplateGroupMatchInfo"/> with match information for the filters applied.</returns>
/// <remarks>
/// Note that filters are applied in the order: group-based filters, template info filters and template parameters filters.
/// If the filter group results in mismatch, the following filter groups won't be applied.
/// </remarks>
internal static TemplateGroupMatchInfo ApplyFilters(
TemplateGroup group,
IEnumerable<Func<TemplateGroup, MatchInfo?>> groupFilters,
IEnumerable<Func<ITemplateInfo, MatchInfo?>> templateInfoFilters,
Func<ITemplateInfo, IEnumerable<MatchInfo>>? templateParametersFilter = null)
{
List<MatchInfo> groupMatchDispositions = new();
foreach (Func<TemplateGroup, MatchInfo?>? filter in groupFilters)
{
MatchInfo? info = filter(group);
if (info != null)
{
groupMatchDispositions.Add(info);
}
}
//if template group is not a match the further evaluation is skipped
if (groupMatchDispositions.Any(x => x.Kind == MatchKind.Mismatch))
{
return new TemplateGroupMatchInfo(
group,
groupMatchDispositions,
Array.Empty<ITemplateMatchInfo>(),
templateMatchEvaluated: false,
templateParameterMatchEvaluated: false);
}
#pragma warning disable CS0618 // Type or member is obsolete
IReadOnlyCollection<ITemplateMatchInfo> templateMatchDispositions = TemplateListFilter.GetTemplateMatchInfo(group.Templates, x => true, templateInfoFilters.ToArray());
#pragma warning restore CS0618 // Type or member is obsolete
//if template info is not a match the further evaluation is skipped
if (templateMatchDispositions.All(template => !TemplateInfoMatch(template)) || templateParametersFilter == null)
{
return new TemplateGroupMatchInfo(
group,
groupMatchDispositions,
templateMatchDispositions,
templateParameterMatchEvaluated: false);
}
foreach (var template in templateMatchDispositions)
{
var parameterMatchInfos = templateParametersFilter(template.Info);
foreach (var parameterMatchInfo in parameterMatchInfos)
{
template.AddMatchDisposition(parameterMatchInfo);
}
}
return new TemplateGroupMatchInfo(group, groupMatchDispositions, templateMatchDispositions.ToList());
}
/// <summary>
/// Returns the template to be invoked based on the command input or <see langword="null"/> if template to invoke cannot be resolved.
/// </summary>
internal (ITemplateInfo Template, IReadOnlyDictionary<string, string?> Parameters)? GetTemplateToInvoke()
{
var templates = GetHighestPrecedenceTemplateMatchInfos();
if (templates.Count() != 1)
{
return null;
}
return (templates.Single().Info, templates.Single().GetValidTemplateParameters());
}
/// <summary>
/// Gets the list of valid choices for <paramref name="parameter"/>.
/// </summary>
/// <param name="parameter">parameter canonical name.</param>
/// <returns>the dictionary of valid choices and descriptions.</returns>
internal IDictionary<string, ParameterChoice> GetValidValuesForChoiceParameter(string parameter)
{
if (!_isTemplateParameterMatchEvaluated)
{
throw new NotSupportedException("Template parameter matches were not evaluated.");
}
Dictionary<string, ParameterChoice> validChoices = new();
foreach (ITemplateMatchInfo template in _templateMatchInfos)
{
ITemplateParameter? choiceParameter = template.Info.GetChoiceParameter(parameter);
if (choiceParameter != null && choiceParameter.Choices != null)
{
foreach (var choice in choiceParameter.Choices)
{
validChoices[choice.Key] = choice.Value;
}
}
}
return validChoices;
}
/// <summary>
/// Gets the highest precedence templates in the group.
/// The templates should match all filter groups: group-based, template-info and template parameters filters.
/// The templates are filtered by preferred language.
/// </summary>
internal IEnumerable<ITemplateInfo> GetHighestPrecedenceTemplates()
{
return GetHighestPrecedenceTemplateMatchInfos().Select(matchInfo => matchInfo.Info);
}
private static bool TemplateInfoMatch(ITemplateMatchInfo templateMatchInfo)
{
return templateMatchInfo.MatchDisposition
.Where(match => match.GetType() != typeof(ParameterMatchInfo))
.All(match => match.Kind != MatchKind.Mismatch);
}
private static bool TemplateParametersMatch(ITemplateMatchInfo templateMatchInfo)
{
return templateMatchInfo.MatchDisposition
.OfType<ParameterMatchInfo>()
.All(match => match.Kind != MatchKind.Mismatch);
}
private IEnumerable<ITemplateMatchInfo> GetHighestPrecedenceTemplateMatchInfos()
{
if (!TemplateMatchInfosWithMatchingParameters.Any())
{
return new List<ITemplateMatchInfo>();
}
IEnumerable<ITemplateMatchInfo> templatesToCheck = FilterTemplatesByPreferredLanguage(TemplateMatchInfosWithMatchingParameters);
int highestPrecedence = templatesToCheck.Max(t => t.Info.Precedence);
return templatesToCheck.Where(t => t.Info.Precedence == highestPrecedence);
}
private IEnumerable<ITemplateMatchInfo> FilterTemplatesByPreferredLanguage(IEnumerable<ITemplateMatchInfo> templatesToFilter)
{
MatchInfo? languageMatch = _groupDispositions.SingleOrDefault(match => match.Name == MatchInfo.BuiltIn.Language);
if ((languageMatch == null) || string.IsNullOrWhiteSpace(languageMatch.Value))
{
return templatesToFilter;
}
else
{
var templatesForPreferredLanguage = templatesToFilter.Where(
matchInfo => matchInfo.Info.GetLanguage()?.Equals(languageMatch.Value, StringComparison.OrdinalIgnoreCase) ?? false);
if (!templatesForPreferredLanguage.Any())
{
return templatesToFilter;
}
return templatesForPreferredLanguage;
}
}
private class OrdinalIgnoreCaseMatchInfoComparer : IEqualityComparer<ParameterMatchInfo>
{
public bool Equals(ParameterMatchInfo? x, ParameterMatchInfo? y)
{
if (x is null && y is null)
{
return true;
}
if (x is null || y is null)
{
return false;
}
return x.Kind == y.Kind
&& string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(x.Value, y.Value, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(ParameterMatchInfo obj)
{
return (obj.Name.ToLowerInvariant(), obj.Value?.ToLowerInvariant(), obj.Kind).GetHashCode();
}
}
}
}
|