feat(packageRules)!: support glob/regex patterns for matchPackageNames (#28551)

BREAKING CHANGE: matchPackageNames exact matches are now case-insensitive
This commit is contained in:
Rhys Arkins 2024-04-23 09:16:45 +02:00
parent adcffd2b6b
commit 1e5cf6d07c
8 changed files with 126 additions and 111 deletions

View file

@ -2950,13 +2950,12 @@ Use this field to restrict rules to a particular package manager. e.g.
}
```
For the full list of available managers, see the [Supported Managers](modules/manager/index.md#supported-managers) documentation.
### matchDepPrefixes
### matchMessage
For log level remapping, use this field to match against the particular log messages.
For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md).
<!-- prettier-ignore -->
!!! note
`matchDepNames` now supports pattern matching and should be used instead.
Use of `matchDepPrefixes` is now deprecated and will be migrated in future.
### matchNewValue
@ -3007,13 +3006,17 @@ For more details on this syntax see Renovate's [string pattern matching document
### matchPackageNames
Use this field if you want to have one or more exact name matches in your package rule.
See also `excludePackageNames`.
Use this field to match against the `packageName` field.
This matching can be an exact match, Glob match, or Regular Expression match.
```json
For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md).
Note that Glob matching (including exact name matching) is case-insensitive.
```json title="exact name match"
{
"packageRules": [
{
"matchDatasources": ["npm"],
"matchPackageNames": ["angular"],
"rangeStrategy": "pin"
}
@ -3021,40 +3024,50 @@ See also `excludePackageNames`.
}
```
The above will configure `rangeStrategy` to `pin` only for the package `angular`.
The above will configure `rangeStrategy` to `pin` only for the npm package `angular`.
<!-- prettier-ignore -->
!!! note
`matchPackageNames` will try matching `packageName` first and then fall back to matching `depName`.
If the fallback is used, Renovate will log a warning, because the fallback will be removed in a future release.
Use `matchDepNames` instead.
### matchPackagePatterns
Use this field if you want to have one or more package names patterns in your package rule.
See also `excludePackagePatterns`.
```json
```json title="prefix match using Glob"
{
"packageRules": [
{
"matchPackagePatterns": ["^angular"],
"rangeStrategy": "replace"
"matchDatasources": ["npm"],
"matchPackageNames": ["@angular/*", "!@angular/abc"],
"groupName": "Angular"
}
]
}
```
The above will configure `rangeStrategy` to `replace` for any package starting with `angular`.
The above will group together any npm package which starts with `@angular/` except `@angular/abc`.
```json title="pattern match using RegEx"
{
"packageRules": [
{
"matchDatasources": ["npm"],
"matchPackageNames": ["/^angular/"],
"groupName": "Angular"
}
]
}
```
The above will group together any npm package which starts with the string `angular`.
### matchPackagePatterns
<!-- prettier-ignore -->
!!! note
`matchPackagePatterns` will try matching `packageName` first and then fall back to matching `depName`.
If the fallback is used, Renovate will log a warning, because the fallback will be removed in a future release.
Use `matchDepPatterns` instead.
`matchPackageNames` now supports pattern matching and should be used instead.
Use of `matchPackagePatterns` is now deprecated and will be migrated in future.
### matchPackagePrefixes
<!-- prettier-ignore -->
!!! note
`matchPackageNames` now supports pattern matching and should be used instead.
Use of `matchPackagePrefixes` is now deprecated and will be migrated in future.
Use this field to match a package prefix without needing to write a regex expression.
See also `excludePackagePrefixes`.
@ -3071,11 +3084,6 @@ See also `excludePackagePrefixes`.
Like the earlier `matchPackagePatterns` example, the above will configure `rangeStrategy` to `replace` for any package starting with `angular`.
<!-- prettier-ignore -->
!!! note
`matchPackagePrefixes` will try matching `packageName` first and then fall back to matching `depName`.
If the fallback is used, Renovate will log a warning, because the fallback will be removed in a future release.
Use `matchDepPatterns` instead.
### matchRepositories

View file

@ -1,5 +1,6 @@
import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { matchRegexOrGlobList } from '../string-match';
import { Matcher } from './base';
export class DepNameMatcher extends Matcher {
@ -13,7 +14,7 @@ export class DepNameMatcher extends Matcher {
if (is.undefined(depName)) {
return false;
}
return matchDepNames.includes(depName);
return matchRegexOrGlobList(depName, matchDepNames);
}
override excludes(
@ -26,6 +27,6 @@ export class DepNameMatcher extends Matcher {
if (is.undefined(depName)) {
return false;
}
return excludeDepNames.includes(depName);
return matchRegexOrGlobList(depName, excludeDepNames);
}
}

View file

@ -15,6 +15,30 @@ describe('util/package-rules/dep-patterns', () => {
);
expect(result).toBeFalse();
});
it('should massage wildcards', () => {
const result = depPatternsMatcher.matches(
{
depName: 'http',
},
{
matchDepPatterns: ['*'],
},
);
expect(result).toBeTrue();
});
it('should convert to regex', () => {
const result = depPatternsMatcher.matches(
{
depName: 'http',
},
{
matchDepPatterns: ['^h'],
},
);
expect(result).toBeTrue();
});
});
describe('exclude', () => {
@ -29,5 +53,29 @@ describe('util/package-rules/dep-patterns', () => {
);
expect(result).toBeFalse();
});
it('should massage wildcards', () => {
const result = depPatternsMatcher.excludes(
{
depName: 'http',
},
{
excludeDepPatterns: ['*'],
},
);
expect(result).toBeTrue();
});
it('should convert to regex', () => {
const result = depPatternsMatcher.excludes(
{
depName: 'http',
},
{
excludeDepPatterns: ['^h'],
},
);
expect(result).toBeTrue();
});
});
});

View file

@ -1,13 +1,11 @@
import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { logger } from '../../logger';
import { regEx } from '../regex';
import { matchRegexOrGlobList } from '../string-match';
import { Matcher } from './base';
import { massagePattern } from './utils';
export class DepPatternsMatcher extends Matcher {
override matches(
{ depName, updateType }: PackageRuleInputConfig,
{ depName }: PackageRuleInputConfig,
{ matchDepPatterns }: PackageRule,
): boolean | null {
if (is.undefined(matchDepPatterns)) {
@ -18,22 +16,16 @@ export class DepPatternsMatcher extends Matcher {
return false;
}
let isMatch = false;
for (const packagePattern of matchDepPatterns) {
const packageRegex = regEx(massagePattern(packagePattern));
if (packageRegex.test(depName)) {
logger.trace(`${depName} matches against ${String(packageRegex)}`);
isMatch = true;
}
}
return isMatch;
const massagedPatterns = matchDepPatterns.map((pattern) =>
pattern === '^*$' || pattern === '*' ? '*' : `/${pattern}/`,
);
return matchRegexOrGlobList(depName, massagedPatterns);
}
override excludes(
{ depName, updateType }: PackageRuleInputConfig,
{ depName }: PackageRuleInputConfig,
{ excludeDepPatterns }: PackageRule,
): boolean | null {
// ignore lockFileMaintenance for backwards compatibility
if (is.undefined(excludeDepPatterns)) {
return null;
}
@ -41,14 +33,9 @@ export class DepPatternsMatcher extends Matcher {
return false;
}
let isMatch = false;
for (const pattern of excludeDepPatterns) {
const packageRegex = regEx(massagePattern(pattern));
if (packageRegex.test(depName)) {
logger.trace(`${depName} matches against ${String(packageRegex)}`);
isMatch = true;
}
}
return isMatch;
const massagedPatterns = excludeDepPatterns.map((pattern) =>
pattern === '^*$' || pattern === '*' ? '*' : `/${pattern}/`,
);
return matchRegexOrGlobList(depName, massagedPatterns);
}
}

View file

@ -1,5 +1,6 @@
import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { matchRegexOrGlobList } from '../string-match';
import { Matcher } from './base';
export class PackageNameMatcher extends Matcher {
@ -14,7 +15,7 @@ export class PackageNameMatcher extends Matcher {
if (!packageName) {
return false;
}
return matchPackageNames.includes(packageName);
return matchRegexOrGlobList(packageName, matchPackageNames);
}
override excludes(
@ -28,6 +29,6 @@ export class PackageNameMatcher extends Matcher {
if (!packageName) {
return false;
}
return excludePackageNames.includes(packageName);
return matchRegexOrGlobList(packageName, excludePackageNames);
}
}

View file

@ -1,22 +1,7 @@
import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { logger } from '../../logger';
import { regEx } from '../regex';
import { matchRegexOrGlobList } from '../string-match';
import { Matcher } from './base';
import { massagePattern } from './utils';
function matchPatternsAgainstName(
matchPackagePatterns: string[],
name: string,
): boolean {
let isMatch = false;
for (const packagePattern of matchPackagePatterns) {
if (isPackagePatternMatch(packagePattern, name)) {
isMatch = true;
}
}
return isMatch;
}
export class PackagePatternsMatcher extends Matcher {
override matches(
@ -32,7 +17,10 @@ export class PackagePatternsMatcher extends Matcher {
return false;
}
return matchPatternsAgainstName(matchPackagePatterns, packageName);
const massagedPatterns = matchPackagePatterns.map((pattern) =>
pattern === '^*$' || pattern === '*' ? '*' : `/${pattern}/`,
);
return matchRegexOrGlobList(packageName, massagedPatterns);
}
override excludes(
@ -48,15 +36,9 @@ export class PackagePatternsMatcher extends Matcher {
return false;
}
return matchPatternsAgainstName(excludePackagePatterns, packageName);
const massagedPatterns = excludePackagePatterns.map((pattern) =>
pattern === '^*$' || pattern === '*' ? '*' : `/${pattern}/`,
);
return matchRegexOrGlobList(packageName, massagedPatterns);
}
}
function isPackagePatternMatch(pckPattern: string, pck: string): boolean {
const re = regEx(massagePattern(pckPattern));
if (re.test(pck)) {
logger.trace(`${pck} matches against ${String(re)}`);
return true;
}
return false;
}

View file

@ -1,13 +1,13 @@
import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { matchRegexOrGlobList } from '../string-match';
import { Matcher } from './base';
export class PackagePrefixesMatcher extends Matcher {
override matches(
{ depName, packageName }: PackageRuleInputConfig,
packageRule: PackageRule,
{ packageName }: PackageRuleInputConfig,
{ matchPackagePrefixes }: PackageRule,
): boolean | null {
const { matchPackagePrefixes } = packageRule;
if (is.undefined(matchPackagePrefixes)) {
return null;
}
@ -16,14 +16,10 @@ export class PackagePrefixesMatcher extends Matcher {
return false;
}
if (
is.string(packageName) &&
matchPackagePrefixes.some((prefix) => packageName.startsWith(prefix))
) {
return true;
}
return false;
const massagedPatterns = matchPackagePrefixes.map(
(pattern) => `${pattern}**`,
);
return matchRegexOrGlobList(packageName, massagedPatterns);
}
override excludes(
@ -38,13 +34,9 @@ export class PackagePrefixesMatcher extends Matcher {
return false;
}
if (
is.string(packageName) &&
excludePackagePrefixes.some((prefix) => packageName.startsWith(prefix))
) {
return true;
}
return false;
const massagedPatterns = excludePackagePrefixes.map(
(pattern) => `${pattern}**`,
);
return matchRegexOrGlobList(packageName, massagedPatterns);
}
}

View file

@ -33,7 +33,3 @@ export function matcherOR(
}
return matchApplied ? false : null;
}
export function massagePattern(pattern: string): string {
return pattern === '^*$' || pattern === '*' ? '.*' : pattern;
}