diff --git a/lib/util/package-rules.ts b/lib/util/package-rules.ts
deleted file mode 100644
index 55b637e5f9847834df7937deec45ce3193df7e4c..0000000000000000000000000000000000000000
--- a/lib/util/package-rules.ts
+++ /dev/null
@@ -1,334 +0,0 @@
-import is from '@sindresorhus/is';
-import minimatch from 'minimatch';
-import slugify from 'slugify';
-import { mergeChildConfig } from '../config';
-import type { PackageRule, PackageRuleInputConfig } from '../config/types';
-import { logger } from '../logger';
-import * as allVersioning from '../modules/versioning';
-import { configRegexPredicate, regEx } from './regex';
-
-function matchesRule(
-  inputConfig: PackageRuleInputConfig,
-  packageRule: PackageRule
-): boolean {
-  const {
-    versioning,
-    packageFile,
-    lockFiles,
-    depType,
-    depTypes,
-    depName,
-    currentValue,
-    currentVersion,
-    lockedVersion,
-    updateType,
-    isBump,
-    sourceUrl,
-    language,
-    baseBranch,
-    manager,
-    datasource,
-  } = inputConfig;
-  const unconstrainedValue = !!lockedVersion && is.undefined(currentValue);
-  // Setting empty arrays simplifies our logic later
-  const matchFiles = packageRule.matchFiles ?? [];
-  const matchPaths = packageRule.matchPaths ?? [];
-  const matchLanguages = packageRule.matchLanguages ?? [];
-  const matchBaseBranches = packageRule.matchBaseBranches ?? [];
-  const matchManagers = packageRule.matchManagers ?? [];
-  const matchDatasources = packageRule.matchDatasources ?? [];
-  const matchDepTypes = packageRule.matchDepTypes ?? [];
-  const matchPackageNames = packageRule.matchPackageNames ?? [];
-  let matchPackagePatterns = packageRule.matchPackagePatterns ?? [];
-  const matchPackagePrefixes = packageRule.matchPackagePrefixes ?? [];
-  const excludePackageNames = packageRule.excludePackageNames ?? [];
-  const excludePackagePatterns = packageRule.excludePackagePatterns ?? [];
-  const excludePackagePrefixes = packageRule.excludePackagePrefixes ?? [];
-  const matchSourceUrlPrefixes = packageRule.matchSourceUrlPrefixes ?? [];
-  const matchSourceUrls = packageRule.matchSourceUrls ?? [];
-  const matchCurrentVersion = packageRule.matchCurrentVersion ?? null;
-  const matchUpdateTypes = packageRule.matchUpdateTypes ?? [];
-  let positiveMatch = false;
-  // Massage a positive patterns patch if an exclude one is present
-  if (
-    (excludePackageNames.length ||
-      excludePackagePatterns.length ||
-      excludePackagePrefixes.length) &&
-    !(
-      matchPackageNames.length ||
-      matchPackagePatterns.length ||
-      matchPackagePrefixes.length
-    )
-  ) {
-    matchPackagePatterns = ['.*'];
-  }
-  if (matchFiles.length) {
-    const isMatch = matchFiles.some(
-      (fileName) =>
-        packageFile === fileName ||
-        (is.array(lockFiles) && lockFiles?.includes(fileName))
-    );
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchPaths.length && packageFile) {
-    const isMatch = matchPaths.some(
-      (rulePath) =>
-        packageFile.includes(rulePath) ||
-        minimatch(packageFile, rulePath, { dot: true })
-    );
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchDepTypes.length) {
-    const isMatch =
-      (depType && matchDepTypes.includes(depType)) ||
-      depTypes?.some((dt) => matchDepTypes.includes(dt));
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchLanguages.length) {
-    if (!language) {
-      return false;
-    }
-    const isMatch = matchLanguages.includes(language);
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchBaseBranches.length) {
-    if (!baseBranch) {
-      return false;
-    }
-    const isMatch = matchBaseBranches.some((matchBaseBranch): boolean => {
-      const isAllowedPred = configRegexPredicate(matchBaseBranch);
-      if (isAllowedPred) {
-        return isAllowedPred(baseBranch);
-      }
-      return matchBaseBranch === baseBranch;
-    });
-
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchManagers.length) {
-    if (!manager) {
-      return false;
-    }
-    const isMatch = matchManagers.includes(manager);
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchDatasources.length) {
-    if (!datasource) {
-      return false;
-    }
-    const isMatch = matchDatasources.includes(datasource);
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchUpdateTypes.length) {
-    const isMatch =
-      (updateType && matchUpdateTypes.includes(updateType)) ||
-      (isBump && matchUpdateTypes.includes('bump'));
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (
-    matchPackageNames.length ||
-    matchPackagePatterns.length ||
-    matchPackagePrefixes.length
-  ) {
-    if (!depName) {
-      // if using the default rules, return true else false
-      return (
-        is.undefined(packageRule.matchPackagePatterns) &&
-        is.undefined(packageRule.matchPackageNames) &&
-        is.undefined(packageRule.matchPackagePrefixes)
-      );
-    }
-    let isMatch = matchPackageNames.includes(depName);
-    // name match is "or" so we check patterns if we didn't match names
-    if (!isMatch) {
-      for (const packagePattern of matchPackagePatterns) {
-        const packageRegex = regEx(
-          packagePattern === '^*$' || packagePattern === '*'
-            ? '.*'
-            : packagePattern
-        );
-        if (packageRegex.test(depName)) {
-          logger.trace(`${depName} matches against ${String(packageRegex)}`);
-          isMatch = true;
-        }
-      }
-    }
-    // prefix match is also "or"
-    if (!isMatch && matchPackagePrefixes.length) {
-      isMatch = matchPackagePrefixes.some((prefix) =>
-        depName.startsWith(prefix)
-      );
-    }
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (excludePackageNames.length) {
-    const isMatch = depName && excludePackageNames.includes(depName);
-    if (isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (depName && excludePackagePatterns.length) {
-    let isMatch = false;
-    for (const pattern of excludePackagePatterns) {
-      const packageRegex = regEx(
-        pattern === '^*$' || pattern === '*' ? '.*' : pattern
-      );
-      if (packageRegex.test(depName)) {
-        logger.trace(`${depName} matches against ${String(packageRegex)}`);
-        isMatch = true;
-      }
-    }
-    if (isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (depName && excludePackagePrefixes.length) {
-    const isMatch = excludePackagePrefixes.some((prefix) =>
-      depName.startsWith(prefix)
-    );
-    if (isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchSourceUrlPrefixes.length) {
-    const upperCaseSourceUrl = sourceUrl?.toUpperCase();
-    const isMatch = matchSourceUrlPrefixes.some((prefix) =>
-      upperCaseSourceUrl?.startsWith(prefix.toUpperCase())
-    );
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchSourceUrls.length) {
-    const upperCaseSourceUrl = sourceUrl?.toUpperCase();
-    const isMatch = matchSourceUrls.some(
-      (url) => upperCaseSourceUrl === url.toUpperCase()
-    );
-    if (!isMatch) {
-      return false;
-    }
-    positiveMatch = true;
-  }
-  if (matchCurrentVersion) {
-    const version = allVersioning.get(versioning);
-    const matchCurrentVersionStr = matchCurrentVersion.toString();
-    const matchCurrentVersionPred = configRegexPredicate(
-      matchCurrentVersionStr
-    );
-    if (matchCurrentVersionPred) {
-      if (
-        !unconstrainedValue &&
-        (!currentValue || !matchCurrentVersionPred(currentValue))
-      ) {
-        return false;
-      }
-      positiveMatch = true;
-    } else if (version.isVersion(matchCurrentVersionStr)) {
-      let isMatch = false;
-      try {
-        isMatch =
-          unconstrainedValue ||
-          !!(
-            currentValue &&
-            version.matches(matchCurrentVersionStr, currentValue)
-          );
-      } catch (err) {
-        // Do nothing
-      }
-      if (!isMatch) {
-        return false;
-      }
-      positiveMatch = true;
-    } else {
-      const compareVersion =
-        currentValue && version.isVersion(currentValue)
-          ? currentValue // it's a version so we can match against it
-          : lockedVersion ?? currentVersion; // need to match against this currentVersion, if available
-      if (compareVersion) {
-        // istanbul ignore next
-        if (version.isVersion(compareVersion)) {
-          const isMatch = version.matches(compareVersion, matchCurrentVersion);
-          // istanbul ignore if
-          if (!isMatch) {
-            return false;
-          }
-          positiveMatch = true;
-        } else {
-          return false;
-        }
-      } else {
-        logger.debug(
-          { matchCurrentVersionStr, currentValue },
-          'Could not find a version to compare'
-        );
-        return false;
-      }
-    }
-  }
-  return positiveMatch;
-}
-
-export function applyPackageRules<T extends PackageRuleInputConfig>(
-  inputConfig: T
-): T {
-  let config = { ...inputConfig };
-  const packageRules = config.packageRules ?? [];
-  logger.trace(
-    { dependency: config.depName, packageRules },
-    `Checking against ${packageRules.length} packageRules`
-  );
-  packageRules.forEach((packageRule) => {
-    // This rule is considered matched if there was at least one positive match and no negative matches
-    if (matchesRule(config, packageRule)) {
-      // Package rule config overrides any existing config
-      const toApply = { ...packageRule };
-      if (config.groupSlug && packageRule.groupName && !packageRule.groupSlug) {
-        // Need to apply groupSlug otherwise the existing one will take precedence
-        toApply.groupSlug = slugify(packageRule.groupName, {
-          lower: true,
-        });
-      }
-      config = mergeChildConfig(config, toApply);
-      delete config.matchPackageNames;
-      delete config.matchPackagePatterns;
-      delete config.matchPackagePrefixes;
-      delete config.excludePackageNames;
-      delete config.excludePackagePatterns;
-      delete config.excludePackagePrefixes;
-      delete config.matchDepTypes;
-      delete config.matchCurrentVersion;
-    }
-  });
-  return config;
-}
diff --git a/lib/util/package-rules/base-branches.ts b/lib/util/package-rules/base-branches.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e712b6f3e72ccf14b7f5cfd802fce54fd2d5af79
--- /dev/null
+++ b/lib/util/package-rules/base-branches.ts
@@ -0,0 +1,27 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { configRegexPredicate } from '../regex';
+import { Matcher } from './base';
+
+export class BaseBranchesMatcher extends Matcher {
+  override matches(
+    { baseBranch }: PackageRuleInputConfig,
+    { matchBaseBranches }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchBaseBranches)) {
+      return null;
+    }
+
+    if (is.undefined(baseBranch)) {
+      return false;
+    }
+
+    return matchBaseBranches.some((matchBaseBranch): boolean => {
+      const isAllowedPred = configRegexPredicate(matchBaseBranch);
+      if (isAllowedPred) {
+        return isAllowedPred(baseBranch);
+      }
+      return matchBaseBranch === baseBranch;
+    });
+  }
+}
diff --git a/lib/util/package-rules/base.ts b/lib/util/package-rules/base.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fa176b504f6d4eb35724e2eb447a1a3c4abf5979
--- /dev/null
+++ b/lib/util/package-rules/base.ts
@@ -0,0 +1,28 @@
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import type { MatcherApi } from './types';
+
+export abstract class Matcher implements MatcherApi {
+  /**
+   * Test exclusion packageRule against inputConfig
+   * @return null if no rules are defined, true if exclusion should be applied and else false
+   * @param inputConfig
+   * @param packageRule
+   */
+  excludes(
+    inputConfig: PackageRuleInputConfig,
+    packageRule: PackageRule
+  ): boolean | null {
+    return null;
+  }
+
+  /**
+   * Test match packageRule against inputConfig
+   * @return null if no rules are defined, true if match should be applied and else false
+   * @param inputConfig
+   * @param packageRule
+   */
+  abstract matches(
+    inputConfig: PackageRuleInputConfig,
+    packageRule: PackageRule
+  ): boolean | null;
+}
diff --git a/lib/util/package-rules/current-version.spec.ts b/lib/util/package-rules/current-version.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..929e4db3453ecbe882a1627659a34220c32b5d39
--- /dev/null
+++ b/lib/util/package-rules/current-version.spec.ts
@@ -0,0 +1,43 @@
+import pep440 from '../../modules/versioning/pep440';
+import { CurrentVersionMatcher } from './current-version';
+
+describe('util/package-rules/current-version', () => {
+  const matcher = new CurrentVersionMatcher();
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('match', () => {
+    it('return false on version exception', () => {
+      const spy = jest.spyOn(pep440, 'matches').mockImplementationOnce(() => {
+        throw new Error();
+      });
+      const result = matcher.matches(
+        {
+          versioning: 'pep440',
+          currentValue: '===>1.2.3',
+        },
+        {
+          matchCurrentVersion: '1.2.3',
+        }
+      );
+      expect(result).toBeFalse();
+      expect(spy.mock.calls).toHaveLength(1);
+    });
+
+    it('return false if no version could be found', () => {
+      const result = matcher.matches(
+        {
+          versioning: 'pep440',
+          currentValue: 'aaaaaa',
+          lockedVersion: 'bbbbbb',
+        },
+        {
+          matchCurrentVersion: 'bbbbbb',
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+});
diff --git a/lib/util/package-rules/current-version.ts b/lib/util/package-rules/current-version.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0fb65ad8da61220053dd171672a737441462fd48
--- /dev/null
+++ b/lib/util/package-rules/current-version.ts
@@ -0,0 +1,62 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { logger } from '../../logger';
+import * as allVersioning from '../../modules/versioning';
+import { configRegexPredicate } from '../regex';
+import { Matcher } from './base';
+
+export class CurrentVersionMatcher extends Matcher {
+  override matches(
+    {
+      versioning,
+      lockedVersion,
+      currentValue,
+      currentVersion,
+    }: PackageRuleInputConfig,
+    { matchCurrentVersion }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchCurrentVersion)) {
+      return null;
+    }
+
+    if (is.nullOrUndefined(currentValue)) {
+      return false;
+    }
+
+    const isUnconstrainedValue = !!lockedVersion;
+    const version = allVersioning.get(versioning);
+    const matchCurrentVersionStr = matchCurrentVersion.toString();
+    const matchCurrentVersionPred = configRegexPredicate(
+      matchCurrentVersionStr
+    );
+
+    if (matchCurrentVersionPred) {
+      return !(!isUnconstrainedValue && !matchCurrentVersionPred(currentValue));
+    }
+    if (version.isVersion(matchCurrentVersionStr)) {
+      try {
+        return (
+          isUnconstrainedValue ||
+          version.matches(matchCurrentVersionStr, currentValue)
+        );
+      } catch (err) {
+        return false;
+      }
+    }
+
+    const compareVersion = version.isVersion(currentValue)
+      ? currentValue // it's a version so we can match against it
+      : lockedVersion ?? currentVersion; // need to match against this currentVersion, if available
+    if (is.undefined(compareVersion)) {
+      return false;
+    }
+    if (version.isVersion(compareVersion)) {
+      return version.matches(compareVersion, matchCurrentVersion);
+    }
+    logger.debug(
+      { matchCurrentVersionStr, currentValue },
+      'Could not find a version to compare'
+    );
+    return false;
+  }
+}
diff --git a/lib/util/package-rules/datasources.ts b/lib/util/package-rules/datasources.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d3237d2eb691c6dadf875b262e3b05297e617401
--- /dev/null
+++ b/lib/util/package-rules/datasources.ts
@@ -0,0 +1,18 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class DatasourcesMatcher extends Matcher {
+  override matches(
+    { datasource }: PackageRuleInputConfig,
+    { matchDatasources }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchDatasources)) {
+      return null;
+    }
+    if (is.undefined(datasource)) {
+      return false;
+    }
+    return matchDatasources.includes(datasource);
+  }
+}
diff --git a/lib/util/package-rules/dep-types.ts b/lib/util/package-rules/dep-types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b1e705c0afbad08b6477b0c62d51980a142f1e57
--- /dev/null
+++ b/lib/util/package-rules/dep-types.ts
@@ -0,0 +1,19 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class DepTypesMatcher extends Matcher {
+  override matches(
+    { depTypes, depType }: PackageRuleInputConfig,
+    { matchDepTypes }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchDepTypes)) {
+      return null;
+    }
+
+    const result =
+      (depType && matchDepTypes.includes(depType)) ||
+      depTypes?.some((dt) => matchDepTypes.includes(dt));
+    return result ?? false;
+  }
+}
diff --git a/lib/util/package-rules/files.spec.ts b/lib/util/package-rules/files.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..374d2650355465db8caddd9406fb8eeea18e2a4f
--- /dev/null
+++ b/lib/util/package-rules/files.spec.ts
@@ -0,0 +1,19 @@
+import { FilesMatcher } from './files';
+
+describe('util/package-rules/files', () => {
+  const fileMatcher = new FilesMatcher();
+
+  describe('match', () => {
+    it('should return false if packageFile is not defined', () => {
+      const result = fileMatcher.matches(
+        {
+          packageFile: undefined,
+        },
+        {
+          matchFiles: ['frontend/package.json'],
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+});
diff --git a/lib/util/package-rules/files.ts b/lib/util/package-rules/files.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e1ab76f4d70192a2e9dbbe4d8e79118e905a73af
--- /dev/null
+++ b/lib/util/package-rules/files.ts
@@ -0,0 +1,23 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class FilesMatcher extends Matcher {
+  override matches(
+    { packageFile, lockFiles }: PackageRuleInputConfig,
+    { matchFiles }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchFiles)) {
+      return null;
+    }
+    if (is.undefined(packageFile)) {
+      return false;
+    }
+
+    return matchFiles.some(
+      (fileName) =>
+        packageFile === fileName ||
+        (is.array(lockFiles) && lockFiles?.includes(fileName))
+    );
+  }
+}
diff --git a/lib/util/package-rules.spec.ts b/lib/util/package-rules/index.spec.ts
similarity index 98%
rename from lib/util/package-rules.spec.ts
rename to lib/util/package-rules/index.spec.ts
index f444208d353a6e7f5d7784642e440e5cb74b1df3..4bb0f75b61c6e061184bb76112863b51cf62dc4f 100644
--- a/lib/util/package-rules.spec.ts
+++ b/lib/util/package-rules/index.spec.ts
@@ -1,9 +1,9 @@
-import type { PackageRuleInputConfig, UpdateType } from '../config/types';
-import { ProgrammingLanguage } from '../constants';
+import type { PackageRuleInputConfig, UpdateType } from '../../config/types';
+import { ProgrammingLanguage } from '../../constants';
 
-import { DockerDatasource } from '../modules/datasource/docker';
-import { OrbDatasource } from '../modules/datasource/orb';
-import { applyPackageRules } from './package-rules';
+import { DockerDatasource } from '../../modules/datasource/docker';
+import { OrbDatasource } from '../../modules/datasource/orb';
+import { applyPackageRules } from './index';
 
 type TestConfig = PackageRuleInputConfig & {
   x?: number;
@@ -11,7 +11,7 @@ type TestConfig = PackageRuleInputConfig & {
   groupName?: string;
 };
 
-describe('util/package-rules', () => {
+describe('util/package-rules/index', () => {
   const config1: TestConfig = {
     foo: 'bar',
 
diff --git a/lib/util/package-rules/index.ts b/lib/util/package-rules/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2817b9a582b4fb143ce7ad2aeec4943180b1786e
--- /dev/null
+++ b/lib/util/package-rules/index.ts
@@ -0,0 +1,100 @@
+import is from '@sindresorhus/is';
+import slugify from 'slugify';
+import { mergeChildConfig } from '../../config';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { logger } from '../../logger';
+import matchers from './matchers';
+import { matcherOR } from './utils';
+
+function matchesRule(
+  inputConfig: PackageRuleInputConfig,
+  packageRule: PackageRule
+): boolean {
+  let positiveMatch = true;
+  let matchApplied = false;
+  // matches
+  for (const groupMatchers of matchers) {
+    const isMatch = matcherOR(
+      'matches',
+      groupMatchers,
+      inputConfig,
+      packageRule
+    );
+
+    // no rules are defined
+    if (is.nullOrUndefined(isMatch)) {
+      continue;
+    }
+
+    matchApplied = true;
+
+    if (!is.truthy(isMatch)) {
+      positiveMatch = false;
+    }
+  }
+
+  // not a single match rule is defined --> assume to match everything
+  if (!matchApplied) {
+    positiveMatch = true;
+  }
+
+  // nothing has been matched
+  if (!positiveMatch) {
+    return false;
+  }
+
+  // excludes
+  for (const groupExcludes of matchers) {
+    const isExclude = matcherOR(
+      'excludes',
+      groupExcludes,
+      inputConfig,
+      packageRule
+    );
+
+    // no rules are defined
+    if (is.nullOrUndefined(isExclude)) {
+      continue;
+    }
+
+    if (isExclude) {
+      return false;
+    }
+  }
+
+  return positiveMatch;
+}
+
+export function applyPackageRules<T extends PackageRuleInputConfig>(
+  inputConfig: T
+): T {
+  let config = { ...inputConfig };
+  const packageRules = config.packageRules ?? [];
+  logger.trace(
+    { dependency: config.depName, packageRules },
+    `Checking against ${packageRules.length} packageRules`
+  );
+  for (const packageRule of packageRules) {
+    // This rule is considered matched if there was at least one positive match and no negative matches
+    if (matchesRule(config, packageRule)) {
+      // Package rule config overrides any existing config
+      const toApply = { ...packageRule };
+      if (config.groupSlug && packageRule.groupName && !packageRule.groupSlug) {
+        // Need to apply groupSlug otherwise the existing one will take precedence
+        toApply.groupSlug = slugify(packageRule.groupName, {
+          lower: true,
+        });
+      }
+      config = mergeChildConfig(config, toApply);
+      delete config.matchPackageNames;
+      delete config.matchPackagePatterns;
+      delete config.matchPackagePrefixes;
+      delete config.excludePackageNames;
+      delete config.excludePackagePatterns;
+      delete config.excludePackagePrefixes;
+      delete config.matchDepTypes;
+      delete config.matchCurrentVersion;
+    }
+  }
+  return config;
+}
diff --git a/lib/util/package-rules/languages.ts b/lib/util/package-rules/languages.ts
new file mode 100644
index 0000000000000000000000000000000000000000..674963d881d6d5e1e1f11a6732ef58e11b141d71
--- /dev/null
+++ b/lib/util/package-rules/languages.ts
@@ -0,0 +1,18 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class LanguagesMatcher extends Matcher {
+  override matches(
+    { language }: PackageRuleInputConfig,
+    { matchLanguages }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchLanguages)) {
+      return null;
+    }
+    if (is.undefined(language)) {
+      return false;
+    }
+    return matchLanguages.includes(language);
+  }
+}
diff --git a/lib/util/package-rules/managers.ts b/lib/util/package-rules/managers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cd5de99a2ad26e28c8d66bc1bd41010d48d32f52
--- /dev/null
+++ b/lib/util/package-rules/managers.ts
@@ -0,0 +1,18 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class ManagersMatcher extends Matcher {
+  override matches(
+    { manager }: PackageRuleInputConfig,
+    { matchManagers }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchManagers)) {
+      return null;
+    }
+    if (is.undefined(manager) || !manager) {
+      return false;
+    }
+    return matchManagers.includes(manager);
+  }
+}
diff --git a/lib/util/package-rules/matchers.ts b/lib/util/package-rules/matchers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0b0da1c57f68a7d1795f361b746cabd78e493aa
--- /dev/null
+++ b/lib/util/package-rules/matchers.ts
@@ -0,0 +1,35 @@
+import { BaseBranchesMatcher } from './base-branches';
+import { CurrentVersionMatcher } from './current-version';
+import { DatasourcesMatcher } from './datasources';
+import { DepTypesMatcher } from './dep-types';
+import { FilesMatcher } from './files';
+import { LanguagesMatcher } from './languages';
+import { ManagersMatcher } from './managers';
+import { PackageNameMatcher } from './package-names';
+import { PackagePatternsMatcher } from './package-patterns';
+import { PackagePrefixesMatcher } from './package-prefixes';
+import { PathsMatcher } from './paths';
+import { SourceUrlPrefixesMatcher } from './sourceurl-prefixes';
+import { SourceUrlsMatcher } from './sourceurls';
+import type { MatcherApi } from './types';
+import { UpdateTypesMatcher } from './update-types';
+
+const matchers: MatcherApi[][] = [];
+export default matchers;
+
+// each manager under the same key will use a logical OR, if multiple matchers are applied AND will be used
+matchers.push([
+  new PackageNameMatcher(),
+  new PackagePatternsMatcher(),
+  new PackagePrefixesMatcher(),
+]);
+matchers.push([new FilesMatcher()]);
+matchers.push([new PathsMatcher()]);
+matchers.push([new DepTypesMatcher()]);
+matchers.push([new LanguagesMatcher()]);
+matchers.push([new BaseBranchesMatcher()]);
+matchers.push([new ManagersMatcher()]);
+matchers.push([new DatasourcesMatcher()]);
+matchers.push([new UpdateTypesMatcher()]);
+matchers.push([new SourceUrlsMatcher(), new SourceUrlPrefixesMatcher()]);
+matchers.push([new CurrentVersionMatcher()]);
diff --git a/lib/util/package-rules/package-names.spec.ts b/lib/util/package-rules/package-names.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e6641ab94bc6e0f4235930aadfb3a5e027fba9f
--- /dev/null
+++ b/lib/util/package-rules/package-names.spec.ts
@@ -0,0 +1,33 @@
+import { PackageNameMatcher } from './package-names';
+
+describe('util/package-rules/package-names', () => {
+  const packageNameMatcher = new PackageNameMatcher();
+
+  describe('match', () => {
+    it('should return false if packageFile is not defined', () => {
+      const result = packageNameMatcher.matches(
+        {
+          depName: undefined,
+        },
+        {
+          matchPackageNames: ['@opentelemetry/http'],
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+
+  describe('exclude', () => {
+    it('should return false if packageFile is not defined', () => {
+      const result = packageNameMatcher.excludes(
+        {
+          depName: undefined,
+        },
+        {
+          excludePackageNames: ['@opentelemetry/http'],
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+});
diff --git a/lib/util/package-rules/package-names.ts b/lib/util/package-rules/package-names.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2ff39899a3ff86fa281d4a572b63ffc65ccf9a78
--- /dev/null
+++ b/lib/util/package-rules/package-names.ts
@@ -0,0 +1,31 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class PackageNameMatcher extends Matcher {
+  override matches(
+    { depName }: PackageRuleInputConfig,
+    { matchPackageNames }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchPackageNames)) {
+      return null;
+    }
+    if (is.undefined(depName)) {
+      return false;
+    }
+    return matchPackageNames.includes(depName);
+  }
+
+  override excludes(
+    { depName }: PackageRuleInputConfig,
+    { excludePackageNames }: PackageRule
+  ): boolean | null {
+    if (is.undefined(excludePackageNames)) {
+      return null;
+    }
+    if (is.undefined(depName)) {
+      return false;
+    }
+    return excludePackageNames.includes(depName);
+  }
+}
diff --git a/lib/util/package-rules/package-patterns.spec.ts b/lib/util/package-rules/package-patterns.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f5f6614df7cde23cca31b218e0db1ca055604d62
--- /dev/null
+++ b/lib/util/package-rules/package-patterns.spec.ts
@@ -0,0 +1,19 @@
+import { PackagePatternsMatcher } from './package-patterns';
+
+describe('util/package-rules/package-patterns', () => {
+  const packageNameMatcher = new PackagePatternsMatcher();
+
+  describe('match', () => {
+    it('should return false if depName is not defined', () => {
+      const result = packageNameMatcher.matches(
+        {
+          depName: undefined,
+        },
+        {
+          matchPackagePatterns: ['@opentelemetry/http'],
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+});
diff --git a/lib/util/package-rules/package-patterns.ts b/lib/util/package-rules/package-patterns.ts
new file mode 100644
index 0000000000000000000000000000000000000000..498a12bd1738d1ff39f21a5d0aa842c430f728d1
--- /dev/null
+++ b/lib/util/package-rules/package-patterns.ts
@@ -0,0 +1,54 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { logger } from '../../logger';
+import { regEx } from '../regex';
+import { Matcher } from './base';
+import { massagePattern } from './utils';
+
+export class PackagePatternsMatcher extends Matcher {
+  override matches(
+    { depName, updateType }: PackageRuleInputConfig,
+    { matchPackagePatterns }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchPackagePatterns)) {
+      return null;
+    }
+
+    if (is.undefined(depName)) {
+      return false;
+    }
+
+    let isMatch = false;
+    for (const packagePattern of matchPackagePatterns) {
+      const packageRegex = regEx(massagePattern(packagePattern));
+      if (packageRegex.test(depName)) {
+        logger.trace(`${depName} matches against ${String(packageRegex)}`);
+        isMatch = true;
+      }
+    }
+    return isMatch;
+  }
+
+  override excludes(
+    { depName, updateType }: PackageRuleInputConfig,
+    { excludePackagePatterns }: PackageRule
+  ): boolean | null {
+    // ignore lockFileMaintenance for backwards compatibility
+    if (is.undefined(excludePackagePatterns)) {
+      return null;
+    }
+    if (is.undefined(depName)) {
+      return false;
+    }
+
+    let isMatch = false;
+    for (const pattern of excludePackagePatterns) {
+      const packageRegex = regEx(massagePattern(pattern));
+      if (packageRegex.test(depName)) {
+        logger.trace(`${depName} matches against ${String(packageRegex)}`);
+        isMatch = true;
+      }
+    }
+    return isMatch;
+  }
+}
diff --git a/lib/util/package-rules/package-prefixes.spec.ts b/lib/util/package-rules/package-prefixes.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a5640d5e643c954e85f510415984bc05b4f35742
--- /dev/null
+++ b/lib/util/package-rules/package-prefixes.spec.ts
@@ -0,0 +1,33 @@
+import { PackagePrefixesMatcher } from './package-prefixes';
+
+describe('util/package-rules/package-prefixes', () => {
+  const packagePrefixesMatcher = new PackagePrefixesMatcher();
+
+  describe('match', () => {
+    it('should return false if depName is not defined', () => {
+      const result = packagePrefixesMatcher.matches(
+        {
+          depName: undefined,
+        },
+        {
+          matchPackagePrefixes: ['@opentelemetry'],
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+
+  describe('exclude', () => {
+    it('should return false if depName is not defined', () => {
+      const result = packagePrefixesMatcher.excludes(
+        {
+          depName: undefined,
+        },
+        {
+          excludePackagePrefixes: ['@opentelemetry'],
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+});
diff --git a/lib/util/package-rules/package-prefixes.ts b/lib/util/package-rules/package-prefixes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2df3ce7daf36ba8de5a99a73f9d098d7729f7e50
--- /dev/null
+++ b/lib/util/package-rules/package-prefixes.ts
@@ -0,0 +1,33 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class PackagePrefixesMatcher extends Matcher {
+  override matches(
+    { depName }: PackageRuleInputConfig,
+    { matchPackagePrefixes }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchPackagePrefixes)) {
+      return null;
+    }
+    if (is.undefined(depName)) {
+      return false;
+    }
+
+    return matchPackagePrefixes.some((prefix) => depName.startsWith(prefix));
+  }
+
+  override excludes(
+    { depName }: PackageRuleInputConfig,
+    { excludePackagePrefixes }: PackageRule
+  ): boolean | null {
+    if (is.undefined(excludePackagePrefixes)) {
+      return null;
+    }
+    if (is.undefined(depName)) {
+      return false;
+    }
+
+    return excludePackagePrefixes.some((prefix) => depName.startsWith(prefix));
+  }
+}
diff --git a/lib/util/package-rules/paths.spec.ts b/lib/util/package-rules/paths.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bf27a2c618547e758a2e770d62071417c286c3b5
--- /dev/null
+++ b/lib/util/package-rules/paths.spec.ts
@@ -0,0 +1,19 @@
+import { PathsMatcher } from './paths';
+
+describe('util/package-rules/paths', () => {
+  const pathsMatcher = new PathsMatcher();
+
+  describe('match', () => {
+    it('should return false if packageFile is not defined', () => {
+      const result = pathsMatcher.matches(
+        {
+          packageFile: undefined,
+        },
+        {
+          matchPaths: ['opentelemetry/http'],
+        }
+      );
+      expect(result).toBeFalse();
+    });
+  });
+});
diff --git a/lib/util/package-rules/paths.ts b/lib/util/package-rules/paths.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fa30014a231090386c1e2769e1d384b40b5f8497
--- /dev/null
+++ b/lib/util/package-rules/paths.ts
@@ -0,0 +1,24 @@
+import is from '@sindresorhus/is';
+import minimatch from 'minimatch';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class PathsMatcher extends Matcher {
+  override matches(
+    { packageFile }: PackageRuleInputConfig,
+    { matchPaths }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchPaths)) {
+      return null;
+    }
+    if (is.undefined(packageFile)) {
+      return false;
+    }
+
+    return matchPaths.some(
+      (rulePath) =>
+        packageFile.includes(rulePath) ||
+        minimatch(packageFile, rulePath, { dot: true })
+    );
+  }
+}
diff --git a/lib/util/package-rules/sourceurl-prefixes.ts b/lib/util/package-rules/sourceurl-prefixes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1c308fef3fa98047b2584d6d2647f7d732a5d879
--- /dev/null
+++ b/lib/util/package-rules/sourceurl-prefixes.ts
@@ -0,0 +1,22 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class SourceUrlPrefixesMatcher extends Matcher {
+  override matches(
+    { sourceUrl }: PackageRuleInputConfig,
+    { matchSourceUrlPrefixes }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchSourceUrlPrefixes)) {
+      return null;
+    }
+    if (is.undefined(sourceUrl)) {
+      return false;
+    }
+    const upperCaseSourceUrl = sourceUrl?.toUpperCase();
+
+    return matchSourceUrlPrefixes.some((prefix) =>
+      upperCaseSourceUrl?.startsWith(prefix.toUpperCase())
+    );
+  }
+}
diff --git a/lib/util/package-rules/sourceurls.ts b/lib/util/package-rules/sourceurls.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b025d27089ecbba39415ad4009baec46e3baab1a
--- /dev/null
+++ b/lib/util/package-rules/sourceurls.ts
@@ -0,0 +1,22 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class SourceUrlsMatcher extends Matcher {
+  override matches(
+    { sourceUrl }: PackageRuleInputConfig,
+    { matchSourceUrls }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchSourceUrls)) {
+      return null;
+    }
+    if (is.undefined(sourceUrl)) {
+      return false;
+    }
+
+    const upperCaseSourceUrl = sourceUrl?.toUpperCase();
+    return matchSourceUrls.some(
+      (url) => upperCaseSourceUrl === url.toUpperCase()
+    );
+  }
+}
diff --git a/lib/util/package-rules/types.ts b/lib/util/package-rules/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce8c546522222938d23ed449635e2438d3b08110
--- /dev/null
+++ b/lib/util/package-rules/types.ts
@@ -0,0 +1,14 @@
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+
+export type MatchType = 'matches' | 'excludes';
+
+export interface MatcherApi {
+  matches(
+    inputConfig: PackageRuleInputConfig,
+    packageRule: PackageRule
+  ): boolean | null;
+  excludes(
+    inputConfig: PackageRuleInputConfig,
+    packageRule: PackageRule
+  ): boolean | null;
+}
diff --git a/lib/util/package-rules/update-types.ts b/lib/util/package-rules/update-types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..294b6e34b3684c183f7cb3270e6b8c3d8d388069
--- /dev/null
+++ b/lib/util/package-rules/update-types.ts
@@ -0,0 +1,18 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import { Matcher } from './base';
+
+export class UpdateTypesMatcher extends Matcher {
+  override matches(
+    { updateType, isBump }: PackageRuleInputConfig,
+    { matchUpdateTypes }: PackageRule
+  ): boolean | null {
+    if (is.undefined(matchUpdateTypes)) {
+      return null;
+    }
+    return (
+      (is.truthy(updateType) && matchUpdateTypes.includes(updateType)) ||
+      (is.truthy(isBump) && matchUpdateTypes.includes('bump'))
+    );
+  }
+}
diff --git a/lib/util/package-rules/utils.ts b/lib/util/package-rules/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..629486271523a3bc445cab4d0f66a4df854ba7fc
--- /dev/null
+++ b/lib/util/package-rules/utils.ts
@@ -0,0 +1,40 @@
+import is from '@sindresorhus/is';
+import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
+import type { MatchType, MatcherApi } from './types';
+
+export function matcherOR(
+  matchType: MatchType,
+  groupMatchers: MatcherApi[],
+  inputConfig: PackageRuleInputConfig,
+  packageRule: PackageRule
+): boolean | null {
+  let positiveMatch = false;
+  let matchApplied = false;
+  for (const matcher of groupMatchers) {
+    let isMatch;
+    switch (matchType) {
+      case 'excludes':
+        isMatch = matcher.excludes(inputConfig, packageRule);
+        break;
+      case 'matches':
+        isMatch = matcher.matches(inputConfig, packageRule);
+        break;
+    }
+
+    // no rules are defined
+    if (is.nullOrUndefined(isMatch)) {
+      continue;
+    }
+
+    matchApplied = true;
+
+    if (is.truthy(isMatch)) {
+      positiveMatch = true;
+    }
+  }
+  return matchApplied ? positiveMatch : null;
+}
+
+export function massagePattern(pattern: string): string {
+  return pattern === '^*$' || pattern === '*' ? '.*' : pattern;
+}