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; +}