diff --git a/lib/modules/manager/pep621/extract.spec.ts b/lib/modules/manager/pep621/extract.spec.ts index 37bd318ea6e5507192e5efda45a77344e245a252..5eec2566337b590176caeb10394a2b7163bf5f50 100644 --- a/lib/modules/manager/pep621/extract.spec.ts +++ b/lib/modules/manager/pep621/extract.spec.ts @@ -605,5 +605,25 @@ describe('modules/manager/pep621/extract', () => { ], }); }); + + it('should resolve dependencies with template', async () => { + const content = codeBlock` + [project] + name = "{{ name }}" + dynamic = ["version"] + requires-python = ">=3.7" + license = {text = "MIT"} + {# comment #} + dependencies = [ + "blinker", + {% if foo %} + "packaging>=20.9,!=22.0", + {% endif %} + ] + readme = "README.md" + `; + const res = await extractPackageFile(content, 'pyproject.toml'); + expect(res?.deps).toHaveLength(2); + }); }); }); diff --git a/lib/modules/manager/pep621/utils.ts b/lib/modules/manager/pep621/utils.ts index 6df1b79ae4e0187c5fbad0bd19a0a04b92f7c072..a53f9530805cae81204785e7b44b15cdf06870a5 100644 --- a/lib/modules/manager/pep621/utils.ts +++ b/lib/modules/manager/pep621/utils.ts @@ -1,6 +1,7 @@ import is from '@sindresorhus/is'; import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; +import { stripTemplates } from '../../../util/string'; import { parse as parseToml } from '../../../util/toml'; import { PypiDatasource } from '../../datasource/pypi'; import { normalizePythonDepName } from '../../datasource/pypi/common'; @@ -136,7 +137,7 @@ export function parsePyProject( content: string, ): PyProject | null { try { - const jsonMap = parseToml(content); + const jsonMap = parseToml(stripTemplates(content)); return PyProjectSchema.parse(jsonMap); } catch (err) { logger.debug( diff --git a/lib/modules/manager/poetry/artifacts.ts b/lib/modules/manager/poetry/artifacts.ts index fd495a25c98931d68d75e7ab9159c3c2d4c2df0f..d75a66934438a3a809c99a1cbebfe5e60f1a4f07 100644 --- a/lib/modules/manager/poetry/artifacts.ts +++ b/lib/modules/manager/poetry/artifacts.ts @@ -16,6 +16,7 @@ import { getGitEnvironmentVariables } from '../../../util/git/auth'; import { find } from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; import { Result } from '../../../util/result'; +import { stripTemplates } from '../../../util/string'; import { parse as parseToml } from '../../../util/toml'; import { parseUrl } from '../../../util/url'; import { PypiDatasource } from '../../datasource/pypi'; @@ -97,7 +98,7 @@ export function getPoetryRequirement( function getPoetrySources(content: string, fileName: string): PoetrySource[] { let pyprojectFile: PoetryFile; try { - pyprojectFile = parseToml(content) as PoetryFile; + pyprojectFile = parseToml(stripTemplates(content)) as PoetryFile; } catch (err) { logger.debug({ err }, 'Error parsing pyproject.toml file'); return []; diff --git a/lib/modules/manager/poetry/extract.spec.ts b/lib/modules/manager/poetry/extract.spec.ts index 5801cd73c93e6b988f6c6da3d4ca1f076a957d55..ead38910d40e2538d9058bdc9f9a42698e5134cb 100644 --- a/lib/modules/manager/poetry/extract.spec.ts +++ b/lib/modules/manager/poetry/extract.spec.ts @@ -496,5 +496,22 @@ describe('modules/manager/poetry/extract', () => { ]); }); }); + + it('parses package file with template', async () => { + const content = codeBlock` + [tool.poetry] + name = "{{ name }}" + {# comment #} + [tool.poetry.dependencies] + python = "^3.9" + {% if foo %} + dep1 = "^1.0.0" + {% endif %} + `; + const res = await extractPackageFile(content, filename); + expect(res?.deps).toHaveLength(2); + expect(res?.deps[0].depName).toBe('python'); + expect(res?.deps[1].depName).toBe('dep1'); + }); }); }); diff --git a/lib/modules/manager/poetry/extract.ts b/lib/modules/manager/poetry/extract.ts index 60c49df062118baaf1e189eed8be28345facc166..71fb2f0049fe141c6fd08281b45992de2df052f9 100644 --- a/lib/modules/manager/poetry/extract.ts +++ b/lib/modules/manager/poetry/extract.ts @@ -7,6 +7,7 @@ import { readLocalFile, } from '../../../util/fs'; import { Result } from '../../../util/result'; +import { stripTemplates } from '../../../util/string'; import { GithubReleasesDatasource } from '../../datasource/github-releases'; import type { PackageFileContent } from '../types'; import { Lockfile, PoetrySchemaToml } from './schema'; @@ -17,7 +18,7 @@ export async function extractPackageFile( ): Promise<PackageFileContent | null> { logger.trace(`poetry.extractPackageFile(${packageFile})`); const { val: res, err } = Result.parse( - content, + stripTemplates(content), PoetrySchemaToml.transform(({ packageFileContent }) => packageFileContent), ).unwrap(); if (err) { diff --git a/lib/util/string.spec.ts b/lib/util/string.spec.ts index 9df3c52ac209371c740da67975e60760c9f56251..dc34cd8f8047d11cc48cc65a5c767dc19a041889 100644 --- a/lib/util/string.spec.ts +++ b/lib/util/string.spec.ts @@ -1,4 +1,4 @@ -import { coerceString, looseEquals, replaceAt } from './string'; +import { coerceString, looseEquals, replaceAt, stripTemplates } from './string'; describe('util/string', () => { describe('replaceAt', () => { @@ -40,4 +40,34 @@ describe('util/string', () => { expect(coerceString(null)).toBe(''); expect(coerceString(null, 'foo')).toBe('foo'); }); + + describe('stripTemplates', () => { + test.each` + input | expected + ${'This is {% template %} text.'} | ${'This is text.'} + ${'This is {%` template `%} text.'} | ${'This is text.'} + ${'Calculate {{ sum }} of numbers.'} | ${'Calculate of numbers.'} + ${'Calculate {{` sum `}} of numbers.'} | ${'Calculate of numbers.'} + ${'Text with {# comment #} embedded comment.'} | ${'Text with embedded comment.'} + ${'Start {{ value }} middle {% code %} end {# note #}.'} | ${'Start middle end .'} + ${'Nested {{ {% pattern %} }} test.'} | ${'Nested test.'} + ${'Line before {%\n multi-line\n pattern\n%} line after.'} | ${'Line before line after.'} + ${'Plain text with no patterns.'} | ${'Plain text with no patterns.'} + ${'Overlap {# comment {% nested %} #} test.'} | ${'Overlap test.'} + ${'Unmatched {% pattern missing end.'} | ${'Unmatched {% pattern missing end.'} + ${'Unmatched pattern missing start %}.'} | ${'Unmatched pattern missing start %}.'} + ${'{{ first }}{% second %}{# third #}Final text.'} | ${'Final text.'} + ${'Empty patterns {% %}{{ }}{# #}.'} | ${'Empty patterns .'} + ${'{% start %} Middle text {# end #}'} | ${' Middle text '} + ${'{% a %}{{ b }}{# c #}'} | ${''} + ${'{%` only `%}{{` patterns `}}{# here #}'} | ${''} + ${'Escaped \\{% not a pattern %\\} text.'} | ${'Escaped \\{% not a pattern %\\} text.'} + ${'Content with escaped \\{\\{ braces \\}\\}.'} | ${'Content with escaped \\{\\{ braces \\}\\}.'} + ${'Unicode {{ 🚀🌟 }} characters.'} | ${'Unicode characters.'} + ${'Special {{ $^.*+?()[]{}|\\ }} characters.'} | ${'Special characters.'} + ${'{% entire text %}'} | ${''} + `('"$input" -> "$expected"', ({ input, expected }) => { + expect(stripTemplates(input)).toBe(expected); + }); + }); }); diff --git a/lib/util/string.ts b/lib/util/string.ts index 4ad6797651b067d881a7a3f646461ca5dfb35496..451fd5091fe5cce5dfb80be3b93a22f1ceeeb79a 100644 --- a/lib/util/string.ts +++ b/lib/util/string.ts @@ -90,3 +90,81 @@ export function coerceString( ): string { return val ?? def ?? ''; } + +/** + * Remove templates from string. + * + * This is more performant version of this code: + * + * ``` + * content + * .replaceAll(regEx(/{{`.+?`}}/gs), '') + * .replaceAll(regEx(/{{.+?}}/gs), '') + * .replaceAll(regEx(/{%`.+?`%}/gs), '') + * .replaceAll(regEx(/{%.+?%}/gs), '') + * .replaceAll(regEx(/{#.+?#}/gs), '') + * ``` + */ +export function stripTemplates(content: string): string { + const result: string[] = []; + + const len = content.length; + let idx = 0; + let lastPos = 0; // Tracks the start index of the next chunk to push + while (idx < len) { + if (content[idx] === '{' && idx + 1 < len) { + let closing: string | undefined; + let skipLength = 0; + + if (content[idx + 1] === '%') { + if (idx + 2 < len && content[idx + 2] === '`') { + // Handle `{%` ... `%}` + closing = '`%}'; + skipLength = 3; + } else { + // Handle `{% ... %}` + closing = '%}'; + skipLength = 2; + } + } else if (content[idx + 1] === '{') { + if (idx + 2 < len && content[idx + 2] === '`') { + // Handle `{{` ... `}}` + closing = '`}}'; + skipLength = 3; + } else { + // Handle `{{ ... }}` + closing = '}}'; + skipLength = 2; + } + } else if (content[idx + 1] === '#') { + // Handle `{# ... #}` + closing = '#}'; + skipLength = 2; + } + + if (closing) { + const end = content.indexOf(closing, idx + skipLength); + if (end !== -1) { + // Append the content before the pattern + if (idx > lastPos) { + result.push(content.slice(lastPos, idx)); + } + + // Move `idx` past the closing tag + idx = end + closing.length; + lastPos = idx; // Update the last position + continue; + } + } + } + + idx++; + } + + // Append any remaining content after the last pattern + if (lastPos < len) { + result.push(content.slice(lastPos)); + } + + return result.join(''); +} diff --git a/lib/util/yaml.ts b/lib/util/yaml.ts index ff78f31db54246f9049f93902d8ad1de33f30bd0..cd898fad46df26ff02baaba6f8e9d0bbc95f5bde 100644 --- a/lib/util/yaml.ts +++ b/lib/util/yaml.ts @@ -10,6 +10,7 @@ import { parseAllDocuments, parseDocument, stringify } from 'yaml'; import type { ZodType } from 'zod'; import { logger } from '../logger'; import { regEx } from './regex'; +import { stripTemplates } from './string'; export interface YamlOptions< ResT = unknown, @@ -167,12 +168,7 @@ export function dump(obj: any, opts?: DumpOptions): string { function massageContent(content: string, options?: YamlOptions): string { if (options?.removeTemplates) { - return content - .replace(regEx(/\s+{{.+?}}:.+/gs), '') - .replace(regEx(/{{`.+?`}}/gs), '') - .replace(regEx(/{{.+?}}/gs), '') - .replace(regEx(/{%`.+?`%}/gs), '') - .replace(regEx(/{%.+?%}/g), ''); + return stripTemplates(content.replace(regEx(/\s+{{.+?}}:.+/gs), '')); } return content;