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 <!-- prettier-ignore -->
!!! note
For log level remapping, use this field to match against the particular log messages. `matchDepNames` now supports pattern matching and should be used instead.
Use of `matchDepPrefixes` is now deprecated and will be migrated in future.
For more details on supported syntax see Renovate's [string pattern matching documentation](./string-pattern-matching.md).
### matchNewValue ### matchNewValue
@ -3007,13 +3006,17 @@ For more details on this syntax see Renovate's [string pattern matching document
### matchPackageNames ### matchPackageNames
Use this field if you want to have one or more exact name matches in your package rule. Use this field to match against the `packageName` field.
See also `excludePackageNames`. 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": [ "packageRules": [
{ {
"matchDatasources": ["npm"],
"matchPackageNames": ["angular"], "matchPackageNames": ["angular"],
"rangeStrategy": "pin" "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 --> ```json title="prefix match using Glob"
!!! 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
{ {
"packageRules": [ "packageRules": [
{ {
"matchPackagePatterns": ["^angular"], "matchDatasources": ["npm"],
"rangeStrategy": "replace" "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 --> <!-- prettier-ignore -->
!!! note !!! note
`matchPackagePatterns` will try matching `packageName` first and then fall back to matching `depName`. `matchPackageNames` now supports pattern matching and should be used instead.
If the fallback is used, Renovate will log a warning, because the fallback will be removed in a future release. Use of `matchPackagePatterns` is now deprecated and will be migrated in future.
Use `matchDepPatterns` instead.
### matchPackagePrefixes ### 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. Use this field to match a package prefix without needing to write a regex expression.
See also `excludePackagePrefixes`. 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`. 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 ### matchRepositories

View file

@ -1,5 +1,6 @@
import is from '@sindresorhus/is'; import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { matchRegexOrGlobList } from '../string-match';
import { Matcher } from './base'; import { Matcher } from './base';
export class DepNameMatcher extends Matcher { export class DepNameMatcher extends Matcher {
@ -13,7 +14,7 @@ export class DepNameMatcher extends Matcher {
if (is.undefined(depName)) { if (is.undefined(depName)) {
return false; return false;
} }
return matchDepNames.includes(depName); return matchRegexOrGlobList(depName, matchDepNames);
} }
override excludes( override excludes(
@ -26,6 +27,6 @@ export class DepNameMatcher extends Matcher {
if (is.undefined(depName)) { if (is.undefined(depName)) {
return false; 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(); 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', () => { describe('exclude', () => {
@ -29,5 +53,29 @@ describe('util/package-rules/dep-patterns', () => {
); );
expect(result).toBeFalse(); 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 is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { logger } from '../../logger'; import { matchRegexOrGlobList } from '../string-match';
import { regEx } from '../regex';
import { Matcher } from './base'; import { Matcher } from './base';
import { massagePattern } from './utils';
export class DepPatternsMatcher extends Matcher { export class DepPatternsMatcher extends Matcher {
override matches( override matches(
{ depName, updateType }: PackageRuleInputConfig, { depName }: PackageRuleInputConfig,
{ matchDepPatterns }: PackageRule, { matchDepPatterns }: PackageRule,
): boolean | null { ): boolean | null {
if (is.undefined(matchDepPatterns)) { if (is.undefined(matchDepPatterns)) {
@ -18,22 +16,16 @@ export class DepPatternsMatcher extends Matcher {
return false; return false;
} }
let isMatch = false; const massagedPatterns = matchDepPatterns.map((pattern) =>
for (const packagePattern of matchDepPatterns) { pattern === '^*$' || pattern === '*' ? '*' : `/${pattern}/`,
const packageRegex = regEx(massagePattern(packagePattern)); );
if (packageRegex.test(depName)) { return matchRegexOrGlobList(depName, massagedPatterns);
logger.trace(`${depName} matches against ${String(packageRegex)}`);
isMatch = true;
}
}
return isMatch;
} }
override excludes( override excludes(
{ depName, updateType }: PackageRuleInputConfig, { depName }: PackageRuleInputConfig,
{ excludeDepPatterns }: PackageRule, { excludeDepPatterns }: PackageRule,
): boolean | null { ): boolean | null {
// ignore lockFileMaintenance for backwards compatibility
if (is.undefined(excludeDepPatterns)) { if (is.undefined(excludeDepPatterns)) {
return null; return null;
} }
@ -41,14 +33,9 @@ export class DepPatternsMatcher extends Matcher {
return false; return false;
} }
let isMatch = false; const massagedPatterns = excludeDepPatterns.map((pattern) =>
for (const pattern of excludeDepPatterns) { pattern === '^*$' || pattern === '*' ? '*' : `/${pattern}/`,
const packageRegex = regEx(massagePattern(pattern)); );
if (packageRegex.test(depName)) { return matchRegexOrGlobList(depName, massagedPatterns);
logger.trace(`${depName} matches against ${String(packageRegex)}`);
isMatch = true;
}
}
return isMatch;
} }
} }

View file

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

View file

@ -1,22 +1,7 @@
import is from '@sindresorhus/is'; import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { logger } from '../../logger'; import { matchRegexOrGlobList } from '../string-match';
import { regEx } from '../regex';
import { Matcher } from './base'; 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 { export class PackagePatternsMatcher extends Matcher {
override matches( override matches(
@ -32,7 +17,10 @@ export class PackagePatternsMatcher extends Matcher {
return false; return false;
} }
return matchPatternsAgainstName(matchPackagePatterns, packageName); const massagedPatterns = matchPackagePatterns.map((pattern) =>
pattern === '^*$' || pattern === '*' ? '*' : `/${pattern}/`,
);
return matchRegexOrGlobList(packageName, massagedPatterns);
} }
override excludes( override excludes(
@ -48,15 +36,9 @@ export class PackagePatternsMatcher extends Matcher {
return false; 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 is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { matchRegexOrGlobList } from '../string-match';
import { Matcher } from './base'; import { Matcher } from './base';
export class PackagePrefixesMatcher extends Matcher { export class PackagePrefixesMatcher extends Matcher {
override matches( override matches(
{ depName, packageName }: PackageRuleInputConfig, { packageName }: PackageRuleInputConfig,
packageRule: PackageRule, { matchPackagePrefixes }: PackageRule,
): boolean | null { ): boolean | null {
const { matchPackagePrefixes } = packageRule;
if (is.undefined(matchPackagePrefixes)) { if (is.undefined(matchPackagePrefixes)) {
return null; return null;
} }
@ -16,14 +16,10 @@ export class PackagePrefixesMatcher extends Matcher {
return false; return false;
} }
if ( const massagedPatterns = matchPackagePrefixes.map(
is.string(packageName) && (pattern) => `${pattern}**`,
matchPackagePrefixes.some((prefix) => packageName.startsWith(prefix)) );
) { return matchRegexOrGlobList(packageName, massagedPatterns);
return true;
}
return false;
} }
override excludes( override excludes(
@ -38,13 +34,9 @@ export class PackagePrefixesMatcher extends Matcher {
return false; return false;
} }
if ( const massagedPatterns = excludePackagePrefixes.map(
is.string(packageName) && (pattern) => `${pattern}**`,
excludePackagePrefixes.some((prefix) => packageName.startsWith(prefix)) );
) { return matchRegexOrGlobList(packageName, massagedPatterns);
return true;
}
return false;
} }
} }

View file

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