diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index a73a5d408d0463eceed2b370a6c95b0a681377d7..1300c46b43d94c8f940d30411b7edfa88c3041aa 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1596,6 +1596,157 @@ Example: } ``` +### matchStringsStrategy + +`matchStringsStrategy` controls behavior when multiple `matchStrings` values are provided. +Three options are available: + +- `any` (default) +- `recursive` +- `combination` + +#### any + +Each provided `matchString` will be matched individually to the content of the `packageFile`. +If a `matchString` has multiple matches in a file each will be interpreted as an independent dependency. + +As example the following configuration will update all 3 lines in the Dockerfile. +renovate.json: + +```json +{ + "regexManagers": [ + { + "fileMatch": ["^Dockerfile$"], + "matchStringsStrategy": "any", + "matchStrings": [ + "ENV [A-Z]+_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s", + "FROM (?<depName>\\S*):(?<currentValue>\\S*)" + ], + "datasourceTemplate": "docker" + } + ] +} +``` + +a Dockerfile: + +```dockerfile +FROM amd64/ubuntu:18.04 +ENV GRADLE_VERSION=6.2 # gradle-version/gradle&versioning=maven +ENV NODE_VERSION=10.19.0 # github-tags/nodejs/node&versioning=node +``` + +#### recursive + +If using `recursive` the `matchStrings` will be looped through and the full match of the last will define the range of the next one. +This can be used to narrow down the search area to prevent multiple matches. +However, the `recursive` strategy still allows the matching of multiple dependencies as described below. +All matches of the first `matchStrings` pattern are detected, then each of these matches will used as basis be used as the input for the next `matchStrings` pattern, and so on. +If the next `matchStrings` pattern has multiple matches then it will split again. +This process will be followed as long there is a match plus a next `matchingStrings` pattern is available or a dependency is detected. + +This is an example how this can work. +The first regex manager will only upgrade `grafana/loki` as looks for the `backup` key then looks for the `test` key and then uses this result for extraction of necessary attributes. +However, the second regex manager will upgrade both definitions as its first `matchStrings` matches both `test` keys. + +renovate.json: + +```json +{ + "regexManagers": [ + { + "fileMatch": ["^example.json$"], + "matchStringsStrategy": "recursive", + "matchStrings": [ + "\"backup\":\\s*{[^}]*}", + "\"test\":\\s*\\{[^}]*}", + "\"name\":\\s*\"(?<depName>.*)\"[^\"]*\"type\":\\s*\"(?<datasource>.*)\"[^\"]*\"value\":\\s*\"(?<currentValue>.*)\"" + ], + "datasourceTemplate": "docker" + }, + { + "fileMatch": ["^example.json$"], + "matchStringsStrategy": "recursive", + "matchStrings": [ + "\"test\":\\s*\\{[^}]*}", + "\"name\":\\s*\"(?<depName>.*)\"[^\"]*\"type\":\\s*\"(?<datasource>.*)\"[^\"]*\"value\":\\s*\"(?<currentValue>.*)\"" + ], + "datasourceTemplate": "docker" + } + ] +} +``` + +example.json: + +```json +{ + "backup": { + "test": { + "name": "grafana/loki", + "type": "docker", + "value": "1.6.1" + } + }, + "setup": { + "test": { + "name": "python", + "type": "docker", + "value": "3.9.0" + } + } +} +``` + +#### combination + +This option allows the possibility to combine the values of multiple lines inside a file. +While using multiple lines is also possible using both other `matchStringStrategy` values, the `combination` approach is less susceptible to white space or line breaks stopping a match. + +`combination` will only match at most one dependency per file, so if you want to update multiple dependencies using `combination` you have to define multiple regex managers. + +Matched group values will be merged to form a single dependency. + +renovate.json: + +```json +{ + "regexManagers": [ + { + "fileMatch": ["^main.yml$"], + "matchStringsStrategy": "combination", + "matchStrings": [ + "prometheus_image:\\s*\"(?<depName>.*)\"\\s*//", + "prometheus_version:\\s*\"(?<currentValue>.*)\"\\s*//" + ], + "datasourceTemplate": "docker" + }, + { + "fileMatch": ["^main.yml$"], + "matchStringsStrategy": "combination", + "matchStrings": [ + "thanos_image:\\s*\"(?<depName>.*)\"\\s*//", + "thanos_version:\\s*\"(?<currentValue>.*)\"\\s*//" + ], + "datasourceTemplate": "docker" + } + ] +} +``` + +Ansible variable file ( yaml ): + +```yaml +prometheus_image: "prom/prometheus" // a comment +prometheus_version: "v2.21.0" // a comment +------ +thanos_image: "prom/prometheus" // a comment +thanos_version: "0.15.0" // a comment +``` + +In the above example, each regex manager will match a single dependency each. + ### depNameTemplate If `depName` cannot be captured with a named capture group in `matchString` then it can be defined manually using this field. diff --git a/lib/config/common.ts b/lib/config/common.ts index 86032544dde66530a0ed9c794ad6a71e5b8e9e3e..26f9205669e0659cf5b837ae8da6dca2b634c63f 100644 --- a/lib/config/common.ts +++ b/lib/config/common.ts @@ -127,6 +127,7 @@ export type RenovateRepository = export interface CustomManager { fileMatch: string[]; matchStrings: string[]; + matchStringsStrategy?: string; depNameTemplate?: string; datasourceTemplate?: string; lookupNameTemplate?: string; @@ -205,6 +206,8 @@ export type UpdateType = | 'rollback' | 'bump'; +export type MatchStringsStrategy = 'any' | 'recursive' | 'combination'; + // TODO: Proper typings export interface PackageRule extends RenovateSharedConfig, diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts index 104c9aaf66766fea38006fc29a0f3dd5cf6625a0..f7cfa954f800880719bfe53e4d8d09543a4589ef 100644 --- a/lib/config/definitions.ts +++ b/lib/config/definitions.ts @@ -1807,6 +1807,15 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchStringsStrategy', + description: 'Strategy how to interpret matchStrings', + type: 'string', + default: 'any', + parent: 'regexManagers', + cli: false, + env: false, + }, { name: 'depNameTemplate', description: diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 0bf9cebffbb737f42e88d741907daca2401020f3..36e63095d6d2b60bd1c8748d836cbc8a74bbc883 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -268,6 +268,7 @@ export async function validateConfig( const allowedKeys = [ 'fileMatch', 'matchStrings', + 'matchStringsStrategy', 'depNameTemplate', 'lookupNameTemplate', 'datasourceTemplate', @@ -289,14 +290,6 @@ export async function validateConfig( ', ' )}`, }); - } else if ( - !regexManager.matchStrings || - regexManager.matchStrings.length !== 1 - ) { - errors.push({ - depName: 'Configuration Error', - message: `Each Regex Manager must contain a matchStrings array of length one`, - }); } else if (!is.nonEmptyArray(regexManager.fileMatch)) { errors.push({ depName: 'Configuration Error', diff --git a/lib/manager/common.ts b/lib/manager/common.ts index 8cfa32501ead316bdeca4ae9e8efeb09a302810c..3bc5c0a9595ad4619bebda5ade5709fe81dd7c15 100644 --- a/lib/manager/common.ts +++ b/lib/manager/common.ts @@ -1,5 +1,10 @@ import { ReleaseType } from 'semver'; -import { GlobalConfig, UpdateType, ValidationMessage } from '../config/common'; +import { + GlobalConfig, + MatchStringsStrategy, + UpdateType, + ValidationMessage, +} from '../config/common'; import { RangeStrategy, SkipReason } from '../types'; import { File } from '../util/git'; @@ -29,6 +34,7 @@ export interface ExtractConfig extends ManagerConfig { export interface CustomExtractConfig extends ExtractConfig { matchStrings: string[]; + matchStringsStrategy?: MatchStringsStrategy; depNameTemplate?: string; lookupNameTemplate?: string; datasourceTemplate?: string; @@ -96,6 +102,7 @@ export interface PackageFile<T = Record<string, any>> yarnrc?: string; yarnWorkspacesPackages?: string[] | string; matchStrings?: string[]; + matchStringsStrategy?: MatchStringsStrategy; } export interface Package<T> extends ManagerData<T> { diff --git a/lib/manager/regex/__fixtures__/ansible.yml b/lib/manager/regex/__fixtures__/ansible.yml new file mode 100644 index 0000000000000000000000000000000000000000..b3647cd99fbeb1b678a160df6f114ada656672c5 --- /dev/null +++ b/lib/manager/regex/__fixtures__/ansible.yml @@ -0,0 +1,5 @@ +prometheus_image: "prom/prometheus" // depName gets initially set +prometheus_version: "v2.21.0" // currentValue get set + +someother_image: "" // will not be set as group value is null/empty string +someother_version: "0.12.0" // overwrites currentValue as later values take precedence. diff --git a/lib/manager/regex/__fixtures__/example.json b/lib/manager/regex/__fixtures__/example.json new file mode 100644 index 0000000000000000000000000000000000000000..d764721e232edb18c887082931a6dc09986143e0 --- /dev/null +++ b/lib/manager/regex/__fixtures__/example.json @@ -0,0 +1,26 @@ +{ + "group1": { + "name": "prom/prometheus", + "type": "docker", + "value": "v2.19.0" + }, + "group2": { + "name": "grafana/grafana", + "type": "docker", + "value": "7.2.2" + }, + "backup": { + "test": { + "name": "grafana/loki", + "type": "docker", + "value": "1.6.1" + } + }, + "setup": { + "test": { + "name": "python", + "type": "docker", + "value": "3.9.0" + } + } +} diff --git a/lib/manager/regex/__snapshots__/index.spec.ts.snap b/lib/manager/regex/__snapshots__/index.spec.ts.snap index 2cbc5039f10181ae9d72d8588d70aa22c2a42416..cb28bedfe3cb5a93e05e76278a8382a44d987471 100644 --- a/lib/manager/regex/__snapshots__/index.spec.ts.snap +++ b/lib/manager/regex/__snapshots__/index.spec.ts.snap @@ -100,3 +100,139 @@ Object { ], } `; + +exports[`manager/regex/index extracts multiple dependencies with multiple matchStrings 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "6.2", + "datasource": "gradle-version", + "depName": "gradle", + "replaceString": "ENV GRADLE_VERSION=6.2 # gradle-version/gradle&versioning=maven +", + "versioning": "maven", + }, + Object { + "currentValue": "10.19.0", + "datasource": "github-tags", + "depName": "nodejs/node", + "replaceString": "ENV NODE_VERSION=10.19.0 # github-tags/nodejs/node&versioning=node +", + "versioning": "node", + }, + ], + "matchStrings": Array [ + "ENV GRADLE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\\\&versioning=(?<versioning>.*?))?\\\\s", + "ENV NODE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\\\&versioning=(?<versioning>.*?))?\\\\s", + ], +} +`; + +exports[`manager/regex/index extracts with combination strategy 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "v2.21.0", + "datasource": "docker", + "depName": "prom/prometheus", + "replaceString": "prometheus_version: \\"v2.21.0\\" //", + }, + ], + "matchStrings": Array [ + "prometheus_image:\\\\s*\\"(?<depName>.*)\\"\\\\s*\\\\/\\\\/", + "prometheus_version:\\\\s*\\"(?<currentValue>.*)\\"\\\\s*\\\\/\\\\/", + ], + "matchStringsStrategy": "combination", +} +`; + +exports[`manager/regex/index extracts with combination strategy and multiple matches 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "0.12.0", + "datasource": "docker", + "depName": "prom/prometheus", + "replaceString": "someother_version: \\"0.12.0\\" //", + }, + ], + "matchStrings": Array [ + ".*_image:\\\\s*\\"(?<depName>.*)\\"\\\\s*\\\\/\\\\/", + ".*_version:\\\\s*\\"(?<currentValue>.*)\\"\\\\s*\\\\/\\\\/", + ], + "matchStringsStrategy": "combination", +} +`; + +exports[`manager/regex/index extracts with recursive strategy and fail because of not sufficient regexes 1`] = `null`; + +exports[`manager/regex/index extracts with recursive strategy and fail because there is no match 1`] = `null`; + +exports[`manager/regex/index extracts with recursive strategy and multiple layers 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "1.6.1", + "datasource": "docker", + "depName": "grafana/loki", + "replaceString": "\\"name\\": \\"grafana/loki\\", + \\"type\\": \\"docker\\", + \\"value\\": \\"1.6.1\\"", + }, + ], + "matchStrings": Array [ + "\\"backup\\":\\\\s*{[^}]*}", + "\\"test\\":\\\\s*\\\\{[^}]*}", + "\\"name\\":\\\\s*\\"(?<depName>.*)\\"[^\\"]*\\"type\\":\\\\s*\\"(?<datasource>.*)\\"[^\\"]*\\"value\\":\\\\s*\\"(?<currentValue>.*)\\"", + ], + "matchStringsStrategy": "recursive", +} +`; + +exports[`manager/regex/index extracts with recursive strategy and multiple matches 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "v2.19.0", + "datasource": "docker", + "depName": "prom/prometheus", + "replaceString": "\\"name\\": \\"prom/prometheus\\", + \\"type\\": \\"docker\\", + \\"value\\": \\"v2.19.0\\"", + }, + Object { + "currentValue": "7.2.2", + "datasource": "docker", + "depName": "grafana/grafana", + "replaceString": "\\"name\\": \\"grafana/grafana\\", + \\"type\\": \\"docker\\", + \\"value\\": \\"7.2.2\\"", + }, + ], + "matchStrings": Array [ + "\\"group.{1}\\":\\\\s*\\\\{[^}]*}", + "\\"name\\":\\\\s*\\"(?<depName>.*)\\"[^\\"]*\\"type\\":\\\\s*\\"(?<datasource>.*)\\"[^\\"]*\\"value\\":\\\\s*\\"(?<currentValue>.*)\\"", + ], + "matchStringsStrategy": "recursive", +} +`; + +exports[`manager/regex/index extracts with recursive strategy and single match 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "v2.19.0", + "datasource": "docker", + "depName": "prom/prometheus", + "replaceString": "\\"name\\": \\"prom/prometheus\\", + \\"type\\": \\"docker\\", + \\"value\\": \\"v2.19.0\\"", + }, + ], + "matchStrings": Array [ + "\\"group1\\":\\\\s*\\\\{[^}]*}", + "\\"name\\":\\\\s*\\"(?<depName>.*)\\"[^\\"]*\\"type\\":\\\\s*\\"(?<datasource>.*)\\"[^\\"]*\\"value\\":\\\\s*\\"(?<currentValue>.*)\\"", + ], + "matchStringsStrategy": "recursive", +} +`; diff --git a/lib/manager/regex/index.spec.ts b/lib/manager/regex/index.spec.ts index 39faae5dda9bfdb1512eec9b29cb81bec45a28de..1716d539792d2a0bb71b7c6db149598ceea336c5 100644 --- a/lib/manager/regex/index.spec.ts +++ b/lib/manager/regex/index.spec.ts @@ -1,12 +1,22 @@ import { readFileSync } from 'fs'; import { resolve } from 'upath'; import { getName } from '../../../test/util'; +import { CustomExtractConfig } from '../common'; import { defaultConfig, extractPackageFile } from '.'; const dockerfileContent = readFileSync( resolve(__dirname, `./__fixtures__/Dockerfile`), 'utf8' ); +const ansibleYamlContent = readFileSync( + resolve(__dirname, `./__fixtures__/ansible.yml`), + 'utf8' +); +const exampleJsonContent = readFileSync( + resolve(__dirname, `./__fixtures__/example.json`), + 'utf8' +); + describe(getName(__filename), () => { it('has default config', () => { expect(defaultConfig).toEqual({ @@ -86,4 +96,136 @@ describe(getName(__filename), () => { ); expect(res).toMatchSnapshot(); }); + it('extracts multiple dependencies with multiple matchStrings', async () => { + const config = { + matchStrings: [ + 'ENV GRADLE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s', + 'ENV NODE_VERSION=(?<currentValue>.*) # (?<datasource>.*?)/(?<depName>.*?)(\\&versioning=(?<versioning>.*?))?\\s', + ], + versioningTemplate: + '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}', + }; + const res = await extractPackageFile( + dockerfileContent, + 'Dockerfile', + config + ); + expect(res).toMatchSnapshot(); + expect(res.deps).toHaveLength(2); + expect( + res.deps.find((dep) => dep.depName === 'nodejs/node').versioning + ).toEqual('node'); + expect(res.deps.find((dep) => dep.depName === 'gradle').versioning).toEqual( + 'maven' + ); + }); + it('extracts with combination strategy', async () => { + const config: CustomExtractConfig = { + matchStrings: [ + 'prometheus_image:\\s*"(?<depName>.*)"\\s*\\/\\/', + 'prometheus_version:\\s*"(?<currentValue>.*)"\\s*\\/\\/', + ], + matchStringsStrategy: 'combination', + datasourceTemplate: 'docker', + }; + const res = await extractPackageFile( + ansibleYamlContent, + 'ansible.yml', + config + ); + expect(res).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); + }); + it('extracts with combination strategy and multiple matches', async () => { + const config: CustomExtractConfig = { + matchStrings: [ + '.*_image:\\s*"(?<depName>.*)"\\s*\\/\\/', + '.*_version:\\s*"(?<currentValue>.*)"\\s*\\/\\/', + ], + matchStringsStrategy: 'combination', + datasourceTemplate: 'docker', + }; + const res = await extractPackageFile( + ansibleYamlContent, + 'ansible.yml', + config + ); + expect(res).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); + }); + it('extracts with recursive strategy and single match', async () => { + const config: CustomExtractConfig = { + matchStrings: [ + '"group1":\\s*\\{[^}]*}', + '"name":\\s*"(?<depName>.*)"[^"]*"type":\\s*"(?<datasource>.*)"[^"]*"value":\\s*"(?<currentValue>.*)"', + ], + matchStringsStrategy: 'recursive', + }; + const res = await extractPackageFile( + exampleJsonContent, + 'example.json', + config + ); + expect(res).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); + }); + it('extracts with recursive strategy and multiple matches', async () => { + const config: CustomExtractConfig = { + matchStrings: [ + '"group.{1}":\\s*\\{[^}]*}', + '"name":\\s*"(?<depName>.*)"[^"]*"type":\\s*"(?<datasource>.*)"[^"]*"value":\\s*"(?<currentValue>.*)"', + ], + matchStringsStrategy: 'recursive', + }; + const res = await extractPackageFile( + exampleJsonContent, + 'example.json', + config + ); + expect(res).toMatchSnapshot(); + expect(res.deps).toHaveLength(2); + }); + it('extracts with recursive strategy and multiple layers ', async () => { + const config: CustomExtractConfig = { + matchStrings: [ + '"backup":\\s*{[^}]*}', + '"test":\\s*\\{[^}]*}', + '"name":\\s*"(?<depName>.*)"[^"]*"type":\\s*"(?<datasource>.*)"[^"]*"value":\\s*"(?<currentValue>.*)"', + ], + matchStringsStrategy: 'recursive', + }; + const res = await extractPackageFile( + exampleJsonContent, + 'example.json', + config + ); + expect(res).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); + }); + it('extracts with recursive strategy and fail because of not sufficient regexes', async () => { + const config: CustomExtractConfig = { + matchStrings: ['"group.{1}":\\s*\\{[^}]*}'], + matchStringsStrategy: 'recursive', + }; + const res = await extractPackageFile( + exampleJsonContent, + 'example.json', + config + ); + expect(res).toMatchSnapshot(); + expect(res).toBeNull(); + }); + it('extracts with recursive strategy and fail because there is no match', async () => { + const config: CustomExtractConfig = { + matchStrings: ['"trunk.{1}":\\s*\\{[^}]*}'], + matchStringsStrategy: 'recursive', + }; + const res = await extractPackageFile( + exampleJsonContent, + 'example.json', + config + ); + expect(res).toMatchSnapshot(); + expect(res).toBeNull(); + }); }); diff --git a/lib/manager/regex/index.ts b/lib/manager/regex/index.ts index 7eb28b8eec4382dd5515e2f5dd6d8dafdb943d71..1b3c724cfab6796d8b20c1bba0864403ddfbf93e 100644 --- a/lib/manager/regex/index.ts +++ b/lib/manager/regex/index.ts @@ -2,62 +2,171 @@ import url from 'url'; import { logger } from '../../logger'; import { regEx } from '../../util/regex'; import * as template from '../../util/template'; -import { CustomExtractConfig, PackageFile, Result } from '../common'; +import { + CustomExtractConfig, + PackageDependency, + PackageFile, + Result, +} from '../common'; export const defaultConfig = { pinDigests: false, }; -export function extractPackageFile( - content: string, - packageFile: string, - config: CustomExtractConfig -): Result<PackageFile | null> { - const regexMatch = regEx(config.matchStrings[0], 'g'); - const deps = []; +const validMatchFields = [ + 'depName', + 'lookupName', + 'currentValue', + 'currentDigest', + 'datasource', + 'versioning', + 'registryUrl', +]; + +function regexMatchAll(regex: RegExp, content: string): RegExpMatchArray[] { + const matches: RegExpMatchArray[] = []; let matchResult; do { - matchResult = regexMatch.exec(content); + matchResult = regex.exec(content); if (matchResult) { - const dep: any = {}; - const { groups } = matchResult; - const fields = [ - 'depName', - 'lookupName', - 'currentValue', - 'currentDigest', - 'datasource', - 'versioning', - 'registryUrl', - ]; - for (const field of fields) { - const fieldTemplate = `${field}Template`; - if (config[fieldTemplate]) { - try { - dep[field] = template.compile(config[fieldTemplate], groups); - } catch (err) { - logger.warn( - { template: config[fieldTemplate] }, - 'Error compiling template for custom manager' - ); - return null; + matches.push(matchResult); + } + } while (matchResult); + return matches; +} + +function createDependency( + matchResult: RegExpMatchArray, + config: CustomExtractConfig, + dep?: PackageDependency +): PackageDependency { + const dependency = dep || {}; + const { groups } = matchResult; + for (const field of validMatchFields) { + const fieldTemplate = `${field}Template`; + if (config[fieldTemplate]) { + try { + dependency[field] = template.compile(config[fieldTemplate], groups); + } catch (err) { + logger.warn( + { template: config[fieldTemplate] }, + 'Error compiling template for custom manager' + ); + return null; + } + } else if (groups[field]) { + switch (field) { + case 'registryUrl': + // check if URL is valid and pack inside an array + if (url.parse(groups[field])) { + dependency.registryUrls = [groups[field]]; } - } else if (groups[field]) { - dep[field] = groups[field]; - } + break; + default: + dependency[field] = groups[field]; + break; } - dep.replaceString = String(matchResult[0]); - if (dep.registryUrl) { - if (url.parse(dep.registryUrl)) { - dep.registryUrls = [dep.registryUrl]; + } + } + dependency.replaceString = String(matchResult[0]); + return dependency; +} + +function mergeDependency(deps: PackageDependency[]): PackageDependency { + const result: PackageDependency = {}; + deps.forEach((dep) => { + validMatchFields.forEach((field) => { + if (dep[field]) { + result[field] = dep[field]; + // save the line replaceString of the section which contains the current Value for a speed up lookup during the replace phase + if (field === 'currentValue') { + result.replaceString = dep.replaceString; } - delete dep.registryUrl; } - deps.push(dep); + }); + }); + return result; +} + +function handleAny( + content: string, + packageFile: string, + config: CustomExtractConfig +): PackageDependency[] { + return config.matchStrings + .map((matchString) => regEx(matchString, 'g')) + .flatMap((regex) => regexMatchAll(regex, content)) // match all regex to content, get all matches, reduce to single array + .map((matchResult) => createDependency(matchResult, config)); +} + +function handleCombination( + content: string, + packageFile: string, + config: CustomExtractConfig +): PackageDependency[] { + const dep = handleAny(content, packageFile, config).reduce( + (mergedDep, currentDep) => mergeDependency([mergedDep, currentDep]), + {} + ); // merge fields of dependencies + return [dep]; +} + +function handleRecursive( + content: string, + packageFile: string, + config: CustomExtractConfig, + index = 0 +): PackageDependency[] { + const regexes = config.matchStrings.map((matchString) => + regEx(matchString, 'g') + ); + // abort if we have no matchString anymore + if (regexes[index] == null) { + return []; + } + return regexMatchAll(regexes[index], content).flatMap((match) => { + // if we have a depName and a currentValue with have the minimal viable definition + if (match?.groups?.depName && match?.groups?.currentValue) { + return createDependency(match, config); } - } while (matchResult); + return handleRecursive(match[0], packageFile, config, index + 1); + }); +} + +export function extractPackageFile( + content: string, + packageFile: string, + config: CustomExtractConfig +): Result<PackageFile | null> { + let deps; + switch (config.matchStringsStrategy) { + default: + case 'any': + deps = handleAny(content, packageFile, config); + break; + case 'combination': + deps = handleCombination(content, packageFile, config); + break; + case 'recursive': + deps = handleRecursive(content, packageFile, config); + break; + } + + // filter all null values + deps = deps.filter(Boolean); if (deps.length) { - return { deps, matchStrings: config.matchStrings }; + if (config.matchStringsStrategy) { + return { + deps, + matchStrings: config.matchStrings, + matchStringsStrategy: config.matchStringsStrategy, + }; + } + return { + deps, + matchStrings: config.matchStrings, + }; } + return null; }