| File: TemplateResolution\TemplateGroupMatchInfo.cs | Web Access |
| Project: src\sdk\src\Cli\Microsoft.TemplateEngine.Cli\Microsoft.TemplateEngine.Cli.csproj (Microsoft.TemplateEngine.Cli) |
// 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(); } } } }