import { logger } from '../../../../logger/index.ts'; import * as fs from '../../../../util/fs/index.ts'; import { newlineRegex, regEx } from '../../../../util/regex.ts'; import { coerceString } from '../../../../util/string.ts'; import type { PackageDependency } from '../../types.ts'; import type { GradleManagerData } from '../types.ts'; import { isDependencyString, versionLikeSubstring } from '../utils.ts'; export const VERSIONS_PROPS = 'versions.props'; export const VERSIONS_LOCK = 'versions.lock'; export const LOCKFIlE_HEADER_TEXT = regEx( /^# Run \.\/gradlew (?:--write-locks|writeVersionsLock|writeVersionsLocks) to regenerate this file/, ); /** * Determines if Palantir gradle-consistent-versions is in use, https://github.com/palantir/gradle-consistent-versions. * Both `versions.props` and `versions.lock` must exist and the special header line of lock file must match * * @param versionsPropsFilename is the full file name path of `versions.props` * @param fileContents map with file contents of all files */ export function usesGcv( versionsPropsFilename: string, fileContents: Record, ): boolean { const versionsLockFile: string = fs.getSiblingFileName( versionsPropsFilename, VERSIONS_LOCK, ); return !!fileContents[versionsLockFile]?.match(LOCKFIlE_HEADER_TEXT); } /** * Confirms whether the provided file name is the props file */ export function isGcvPropsFile(fileName: string): boolean { return fileName === VERSIONS_PROPS || fileName.endsWith(`/${VERSIONS_PROPS}`); } /** * Confirms whether the provided file name is the lock file */ export function isGcvLockFile(fileName: string): boolean { return fileName === VERSIONS_LOCK || fileName.endsWith(`/${VERSIONS_LOCK}`); } /** * Parses Gradle-Consistent-Versions files to figure out what dependencies, versions * and groups they contain. The parsing goes like this: * - Parse `versions.props` into deps (or groups) and versions, remembering file offsets * - Parse `versions.lock` into deps and lock-versions * - For each exact dep in props file, lookup the lock-version from lock file * - For each group/regex dep in props file, lookup the set of exact deps and versions in lock file * * @param propsFileName name and path of the props file * @param fileContents text content of all files */ export function parseGcv( propsFileName: string, fileContents: Record, ): PackageDependency[] { const propsFileContent = coerceString(fileContents[propsFileName]); const lockFileName = fs.getSiblingFileName(propsFileName, VERSIONS_LOCK); const lockFileContent = coerceString(fileContents[lockFileName]); const lockFileMap = parseLockFile(lockFileContent); const [propsFileExactMap, propsFileRegexMap] = parsePropsFile(propsFileContent); const extractedDeps: PackageDependency[] = []; // For each exact dep in props file for (const [propDep, versionAndPosition] of propsFileExactMap) { if (lockFileMap.has(propDep)) { const newDep: Record = { managerData: { packageFile: propsFileName, fileReplacePosition: versionAndPosition.filePos, }, depName: propDep, currentValue: versionAndPosition.version, lockedVersion: lockFileMap.get(propDep)?.version, depType: lockFileMap.get(propDep)?.depType, } satisfies PackageDependency; extractedDeps.push(newDep); // Remove from the lockfile map so the same exact lib will not be included in globbing lockFileMap.delete(propDep); } } // For each regular expression dep in props file (starting with the longest glob string)... for (const [propDepGlob, propVerAndPos] of propsFileRegexMap) { const globRegex = globToRegex(propDepGlob); for (const [exactDep, lockVersionAndDepType] of lockFileMap) { if (globRegex.test(exactDep)) { const newDep: Record = { managerData: { packageFile: propsFileName, fileReplacePosition: propVerAndPos.filePos, }, depName: exactDep, currentValue: propVerAndPos.version, lockedVersion: lockVersionAndDepType.version, depType: lockVersionAndDepType.depType, sharedVariableName: propDepGlob, } satisfies PackageDependency; extractedDeps.push(newDep); // Remove from the lockfile map so the same lib will not be included in more generic globs later lockFileMap.delete(exactDep); } } } return extractedDeps; } // Translate glob syntax to a regex that does the same. Note that we cannot use replaceAll as it does not exist in Node14 // Loosely borrowed mapping to regex from https://github.com/palantir/gradle-consistent-versions/blob/develop/src/main/java/com/palantir/gradle/versions/FuzzyPatternResolver.java function globToRegex(depName: string): RegExp { return regEx( depName .replace(regEx(/\*/g), '_WC_CHAR_') .replace(regEx(/[/\-\\^$*+?.()|[\]{}]/g), '\\$&') .replace(regEx(/_WC_CHAR_/g), '.*?'), ); } interface VersionWithPosition { version: string; filePos: number; } interface VersionWithDepType { version: string; depType: string; } /** * Parses `versions.lock` */ export function parseLockFile(input: string): Map { const lockLineRegex = regEx( `^(?[^:]+:[^:]+):(?[^ ]+) \\(\\d+ constraints: [0-9a-f]+\\)$`, ); const depVerMap = new Map(); let isTestDepType = false; for (const line of input.split(newlineRegex)) { const lineMatch = lockLineRegex.exec(line); if (lineMatch?.groups) { const { depName, lockVersion } = lineMatch.groups; if (isDependencyString(`${depName}:${lockVersion}`)) { depVerMap.set(depName, { version: lockVersion, depType: isTestDepType ? 'test' : 'dependencies', }); } } else if (line === '[Test dependencies]') { isTestDepType = true; // We know that all lines below this header are test dependencies } } logger.trace( `Found ${depVerMap.size} locked dependencies in ${VERSIONS_LOCK}.`, ); return depVerMap; } /** * Parses `versions.props`, this is CR/LF safe * @param input the entire property file from file system * @return two maps, first being exact matches, second regex matches */ export function parsePropsFile( input: string, ): [Map, Map] { const propsLineRegex = regEx( `^(?[^:]+:[^=]+?) *= *(?.*)$`, ); const depVerExactMap = new Map(); const depVerRegexMap = new Map(); let startOfLineIdx = 0; const isCrLf = input.indexOf('\r\n') > 0; const validGlob = regEx(/^[a-zA-Z][-_a-zA-Z0-9.:*]+$/); for (const line of input.split(newlineRegex)) { const lineMatch = propsLineRegex.exec(line); if (lineMatch?.groups) { const { depName, propsVersion } = lineMatch.groups; if ( validGlob.test(depName) && versionLikeSubstring(propsVersion) !== null ) { const startPosInLine = line.lastIndexOf(propsVersion); const propVersionPos = startOfLineIdx + startPosInLine; if (depName.includes('*')) { depVerRegexMap.set(depName, { version: propsVersion, filePos: propVersionPos, }); } else { depVerExactMap.set(depName, { version: propsVersion, filePos: propVersionPos, }); } } } startOfLineIdx += line.length + (isCrLf ? 2 : 1); } logger.trace( `Found ${depVerExactMap.size} dependencies and ${depVerRegexMap.size} wildcard dependencies in ${VERSIONS_PROPS}.`, ); return [ depVerExactMap, new Map( [...depVerRegexMap].sort(([ka], [kb]) => ka.localeCompare(kb)).reverse(), ), ]; }