refactor(package-rules): move to class based implementation (#16865)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
Sebastian Poxhofer 2022-08-29 21:40:03 +02:00 committed by GitHub
parent 92800240be
commit dda2ebce92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 778 additions and 340 deletions

View file

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

View 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;
});
}
}

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

View 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();
});
});
});

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

View 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);
}
}

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

View 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();
});
});
});

View 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))
);
}
}

View file

@ -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',

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

View 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);
}
}

View 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);
}
}

View 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()]);

View 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();
});
});
});

View 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);
}
}

View 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();
});
});
});

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

View 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();
});
});
});

View 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));
}
}

View 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();
});
});
});

View 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 })
);
}
}

View 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())
);
}
}

View 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()
);
}
}

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

View 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'))
);
}
}

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