import upath from 'upath'; import { logger } from '../../../logger/index.ts'; import { coerceArray } from '../../../util/array.ts'; import { getLocalFiles } from '../../../util/fs/index.ts'; import { regEx } from '../../../util/regex.ts'; import { MavenDatasource } from '../../datasource/maven/index.ts'; import gradleVersioning from '../../versioning/gradle/index.ts'; import type { ExtractConfig, PackageDependency, PackageFile, } from '../types.ts'; import { parseCatalog, unifyCatalogSharedVariableNames, } from './extract/catalog.ts'; import { isGcvPropsFile, parseGcv, usesGcv, } from './extract/consistent-versions-plugin.ts'; import { REGISTRY_URLS } from './parser/common.ts'; import { parseGradle, parseKotlinSource, parseProps } from './parser.ts'; import type { ContentDescriptorSpec, GradleManagerData, PackageRegistry, VariableRegistry, } from './types.ts'; import { getVars, isGradleScriptFile, isKotlinSourceFile, isPropsFile, isTOMLFile, reorderFiles, toAbsolutePath, updateVars, updateVarsFromDefaultCatalog, } from './utils.ts'; const mavenDatasource = MavenDatasource.id; function updatePackageRegistries( packageRegistries: PackageRegistry[], urls: PackageRegistry[], ): void { for (const url of urls) { const registryAlreadyKnown = packageRegistries.some( (item) => item.registryUrl === url.registryUrl && item.scope === url.scope && item.registryType === url.registryType && item.content === url.content, ); if (!registryAlreadyKnown) { packageRegistries.push(url); } } } export function matchesContentDescriptor( dep: PackageDependency, contentDescriptors?: ContentDescriptorSpec[], ): boolean { const [groupId, artifactId] = (dep.packageName ?? dep.depName!).split(':'); let hasIncludes = false; let hasExcludes = false; let matchesInclude = false; let matchesExclude = false; for (const content of coerceArray(contentDescriptors)) { const { mode, matcher, groupId: contentGroupId, artifactId: contentArtifactId, version: contentVersion, } = content; // group matching let groupMatch = false; if (matcher === 'regex') { groupMatch = regEx(contentGroupId).test(groupId); } else if (matcher === 'subgroup') { groupMatch = groupId === contentGroupId || `${groupId}.`.startsWith(contentGroupId); } else { groupMatch = groupId === contentGroupId; } // artifact matching (optional) let artifactMatch = true; if (groupMatch && contentArtifactId) { if (matcher === 'regex') { artifactMatch = regEx(contentArtifactId).test(artifactId); } else { artifactMatch = artifactId === contentArtifactId; } } // version matching (optional) let versionMatch = true; if (groupMatch && artifactMatch && contentVersion && dep.currentValue) { if (matcher === 'regex') { versionMatch = regEx(contentVersion).test(dep.currentValue); } else { // contentVersion can be an exact version or a gradle-supported version range versionMatch = gradleVersioning.matches( dep.currentValue, contentVersion, ); } } const isMatch = groupMatch && artifactMatch && versionMatch; if (mode === 'include') { hasIncludes = true; if (isMatch) { matchesInclude = true; } } else if (mode === 'exclude') { hasExcludes = true; if (isMatch) { matchesExclude = true; } } } if (hasIncludes && hasExcludes) { // if both includes and excludes exist, dep must match include and not match exclude return matchesInclude && !matchesExclude; } if (hasIncludes) { // if only includes exist, dep must match at least one include return matchesInclude; } if (hasExcludes) { // if only excludes exist, dep must not match any exclude return !matchesExclude; } // by default, repositories include everything and exclude nothing return true; } function getRegistryUrlsForDep( packageRegistries: PackageRegistry[], dep: PackageDependency, ): string[] { const scope = dep.depType === 'plugin' ? 'plugin' : 'dep'; const matchingRegistries = packageRegistries.filter( (item) => item.scope === scope && matchesContentDescriptor(dep, item.content), ); const exclusiveRegistries = matchingRegistries.filter( (item) => item.registryType === 'exclusive', ); const registryUrls = ( exclusiveRegistries.length ? exclusiveRegistries : matchingRegistries ).map((item) => item.registryUrl); if (!registryUrls.length && scope === 'plugin') { registryUrls.push(REGISTRY_URLS.gradlePluginPortal); } return [...new Set(registryUrls)]; } async function parsePackageFiles( config: ExtractConfig, packageFiles: string[], extractedDeps: PackageDependency[], packageFilesByName: Record, packageRegistries: PackageRegistry[], ): Promise[]> { const varRegistry: VariableRegistry = {}; const fileContents = await getLocalFiles(packageFiles); for (const packageFile of packageFiles) { packageFilesByName[packageFile] = { packageFile, datasource: mavenDatasource, deps: [], }; try { // TODO #22198 const content = fileContents[packageFile]!; const packageFileDir = upath.dirname(toAbsolutePath(packageFile)); if (isPropsFile(packageFile)) { const { vars, deps } = parseProps(content, packageFile); updateVars(varRegistry, packageFileDir, vars); extractedDeps.push(...deps); } else if (isTOMLFile(packageFile)) { const { vars, deps } = parseCatalog(packageFile, content); updateVarsFromDefaultCatalog( varRegistry, packageFileDir, packageFile, vars, ); extractedDeps.push(...deps); } else if ( isGcvPropsFile(packageFile) && usesGcv(packageFile, fileContents) ) { const deps = parseGcv(packageFile, fileContents); extractedDeps.push(...deps); } else if (isKotlinSourceFile(packageFile)) { const vars = getVars(varRegistry, packageFileDir); const { vars: gradleVars, deps } = parseKotlinSource( content, vars, packageFile, ); updateVars(varRegistry, '/', gradleVars); extractedDeps.push(...deps); } else if (isGradleScriptFile(packageFile)) { const vars = getVars(varRegistry, packageFileDir); const { deps, urls, vars: gradleVars, } = parseGradle(content, vars, packageFile, fileContents); updatePackageRegistries(packageRegistries, urls); updateVars(varRegistry, packageFileDir, gradleVars); extractedDeps.push(...deps); } } catch (err) { logger.debug( { err, config, packageFile }, `Failed to process Gradle file`, ); } } return extractedDeps; } export async function extractAllPackageFiles( config: ExtractConfig, packageFiles: string[], ): Promise { const packageFilesByName: Record = {}; const packageRegistries: PackageRegistry[] = []; const extractedDeps: PackageDependency[] = []; const kotlinSourceFiles = packageFiles.filter(isKotlinSourceFile); const gradleFiles = reorderFiles( packageFiles.filter((e) => !kotlinSourceFiles.includes(e)), ); await parsePackageFiles( config, [...kotlinSourceFiles, ...kotlinSourceFiles, ...gradleFiles], extractedDeps, packageFilesByName, packageRegistries, ); if (!extractedDeps.length) { return null; } unifyCatalogSharedVariableNames(extractedDeps); for (const dep of extractedDeps) { dep.fileReplacePosition = dep?.managerData?.fileReplacePosition; // #8224 const key = dep.managerData?.packageFile; // istanbul ignore else if (key) { let pkgFile: PackageFile = packageFilesByName[key]; // istanbul ignore if: won't happen if "apply from" processes only initially known files if (!pkgFile) { pkgFile = { packageFile: key, datasource: mavenDatasource, deps: [], }; } dep.datasource ??= mavenDatasource; if (dep.datasource === mavenDatasource) { dep.registryUrls = getRegistryUrlsForDep(packageRegistries, dep); dep.depType ??= key.startsWith('buildSrc') && !kotlinSourceFiles.length ? 'devDependencies' : 'dependencies'; } const depAlreadyInPkgFile = pkgFile.deps.some( (item) => item.depName === dep.depName && item.managerData?.fileReplacePosition === dep.managerData?.fileReplacePosition, ); if (!depAlreadyInPkgFile) { pkgFile.deps.push(dep); } packageFilesByName[key] = pkgFile; } else { logger.debug({ dep }, `Failed to process Gradle dependency`); } } return Object.values(packageFilesByName); }