From 49afe4e3b2c025133be3db129db6519ca916c861 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sun, 21 Apr 2024 03:28:06 -0300 Subject: [PATCH] refactor(host-rules): Simplify ordering and matching (#28512) Co-authored-by: Rhys Arkins --- lib/util/host-rules.ts | 131 ++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 75 deletions(-) diff --git a/lib/util/host-rules.ts b/lib/util/host-rules.ts index bae2c3f5c160b7..3054c6d698e00a 100644 --- a/lib/util/host-rules.ts +++ b/lib/util/host-rules.ts @@ -1,6 +1,4 @@ import is from '@sindresorhus/is'; -import merge from 'deepmerge'; -import type { SetRequired } from 'type-fest'; import { logger } from '../logger'; import type { CombinedHostRule, HostRule } from '../types'; import { clone } from './clone'; @@ -77,36 +75,6 @@ export interface HostRuleSearch { url?: string; } -function isEmpty( - rule: HostRule, -): rule is Omit { - return !rule.hostType && !rule.resolvedHost; -} - -function isComplete( - rule: HostRule, -): rule is SetRequired { - return !!rule.hostType && !!rule.resolvedHost; -} - -function isOnlyHostType( - rule: HostRule, -): rule is Omit< - SetRequired, - 'matchHost' | 'resolvedHost' -> { - return !!rule.hostType && !rule.resolvedHost; -} - -function isOnlyMatchHost( - rule: HostRule, -): rule is Omit< - SetRequired, - 'hostType' -> { - return !rule.hostType && !!rule.matchHost; -} - function matchesHost(url: string, matchHost: string): boolean { if (isHttpUrl(url) && isHttpUrl(matchHost)) { return url.startsWith(matchHost); @@ -132,56 +100,69 @@ function matchesHost(url: string, matchHost: string): boolean { return hostname.endsWith(topLevelSuffix); } -function prioritizeLongestMatchHost(rule1: HostRule, rule2: HostRule): number { - // istanbul ignore if: won't happen in practice - if (!rule1.matchHost || !rule2.matchHost) { +function fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number { + if (!a.matchHost || !b.matchHost) { return 0; } - return rule1.matchHost.length - rule2.matchHost.length; + return a.matchHost.length - b.matchHost.length; +} + +function hostRuleRank({ hostType, matchHost }: HostRule): number { + if (hostType && matchHost) { + return 3; + } + + if (matchHost) { + return 2; + } + + if (hostType) { + return 1; + } + + return 0; +} + +function fromLowerToHigherRank(a: HostRule, b: HostRule): number { + return hostRuleRank(a) - hostRuleRank(b); } export function find(search: HostRuleSearch): CombinedHostRule { - if (!(!!search.hostType || search.url)) { + if ([search.hostType, search.url].every(is.falsy)) { logger.warn({ search }, 'Invalid hostRules search'); return {}; } - let res: HostRule = {}; - // First, apply empty rule matches - hostRules - .filter((rule) => isEmpty(rule)) - .forEach((rule) => { - res = merge(res, rule); - }); - // Next, find hostType-only matches - hostRules - .filter((rule) => isOnlyHostType(rule) && rule.hostType === search.hostType) - .forEach((rule) => { - res = merge(res, rule); - }); - hostRules - .filter( - (rule) => - isOnlyMatchHost(rule) && - search.url && - matchesHost(search.url, rule.matchHost), - ) - .sort(prioritizeLongestMatchHost) - .forEach((rule) => { - res = merge(res, rule); - }); - // Finally, find combination matches - hostRules - .filter( - (rule) => - isComplete(rule) && - rule.hostType === search.hostType && - search.url && - matchesHost(search.url, rule.matchHost), - ) - .sort(prioritizeLongestMatchHost) - .forEach((rule) => { - res = merge(res, rule); - }); + + // Sort primarily by rank, and secondarily by matchHost length + const sortedRules = hostRules + .sort(fromShorterToLongerMatchHost) + .sort(fromLowerToHigherRank); + + const matchedRules: HostRule[] = []; + for (const rule of sortedRules) { + let hostTypeMatch = true; + let hostMatch = true; + + if (rule.hostType) { + hostTypeMatch = false; + if (search.hostType === rule.hostType) { + hostTypeMatch = true; + } + } + + if (rule.matchHost && rule.resolvedHost) { + hostMatch = false; + if (search.url) { + hostMatch = matchesHost(search.url, rule.matchHost); + } + } + + if (hostTypeMatch && hostMatch) { + matchedRules.push(clone(rule)); + } + } + + const res: HostRule = Object.assign({}, ...matchedRules); delete res.hostType; delete res.resolvedHost; delete res.matchHost; @@ -199,7 +180,7 @@ export function hostType({ url }: { url: string }): string | null { return ( hostRules .filter((rule) => rule.matchHost && matchesHost(url, rule.matchHost)) - .sort(prioritizeLongestMatchHost) + .sort(fromShorterToLongerMatchHost) .map((rule) => rule.hostType) .filter(is.truthy) .pop() ?? null