mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-13 07:26:26 +00:00
refactor(package-rules): move to class based implementation (#16865)
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
parent
92800240be
commit
dda2ebce92
27 changed files with 778 additions and 340 deletions
|
@ -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;
|
||||
}
|
27
lib/util/package-rules/base-branches.ts
Normal file
27
lib/util/package-rules/base-branches.ts
Normal file
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
28
lib/util/package-rules/base.ts
Normal file
28
lib/util/package-rules/base.ts
Normal file
|
@ -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;
|
||||
}
|
43
lib/util/package-rules/current-version.spec.ts
Normal file
43
lib/util/package-rules/current-version.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
62
lib/util/package-rules/current-version.ts
Normal file
62
lib/util/package-rules/current-version.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
18
lib/util/package-rules/datasources.ts
Normal file
18
lib/util/package-rules/datasources.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
19
lib/util/package-rules/dep-types.ts
Normal file
19
lib/util/package-rules/dep-types.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
19
lib/util/package-rules/files.spec.ts
Normal file
19
lib/util/package-rules/files.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
23
lib/util/package-rules/files.ts
Normal file
23
lib/util/package-rules/files.ts
Normal file
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
100
lib/util/package-rules/index.ts
Normal file
100
lib/util/package-rules/index.ts
Normal file
|
@ -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;
|
||||
}
|
18
lib/util/package-rules/languages.ts
Normal file
18
lib/util/package-rules/languages.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
18
lib/util/package-rules/managers.ts
Normal file
18
lib/util/package-rules/managers.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
35
lib/util/package-rules/matchers.ts
Normal file
35
lib/util/package-rules/matchers.ts
Normal file
|
@ -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()]);
|
33
lib/util/package-rules/package-names.spec.ts
Normal file
33
lib/util/package-rules/package-names.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
31
lib/util/package-rules/package-names.ts
Normal file
31
lib/util/package-rules/package-names.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
19
lib/util/package-rules/package-patterns.spec.ts
Normal file
19
lib/util/package-rules/package-patterns.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
54
lib/util/package-rules/package-patterns.ts
Normal file
54
lib/util/package-rules/package-patterns.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
33
lib/util/package-rules/package-prefixes.spec.ts
Normal file
33
lib/util/package-rules/package-prefixes.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
33
lib/util/package-rules/package-prefixes.ts
Normal file
33
lib/util/package-rules/package-prefixes.ts
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
19
lib/util/package-rules/paths.spec.ts
Normal file
19
lib/util/package-rules/paths.spec.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
24
lib/util/package-rules/paths.ts
Normal file
24
lib/util/package-rules/paths.ts
Normal file
|
@ -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 })
|
||||
);
|
||||
}
|
||||
}
|
22
lib/util/package-rules/sourceurl-prefixes.ts
Normal file
22
lib/util/package-rules/sourceurl-prefixes.ts
Normal file
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
22
lib/util/package-rules/sourceurls.ts
Normal file
22
lib/util/package-rules/sourceurls.ts
Normal file
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
14
lib/util/package-rules/types.ts
Normal file
14
lib/util/package-rules/types.ts
Normal file
|
@ -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;
|
||||
}
|
18
lib/util/package-rules/update-types.ts
Normal file
18
lib/util/package-rules/update-types.ts
Normal file
|
@ -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'))
|
||||
);
|
||||
}
|
||||
}
|
40
lib/util/package-rules/utils.ts
Normal file
40
lib/util/package-rules/utils.ts
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue