2022-12-08 12:21:14 +00:00
import { logger } from '../../../../logger' ;
import * as fs from '../../../../util/fs' ;
import { newlineRegex , regEx } from '../../../../util/regex' ;
2023-09-06 13:35:52 +00:00
import { coerceString } from '../../../../util/string' ;
2022-12-08 12:21:14 +00:00
import type { PackageDependency } from '../../types' ;
import type { GradleManagerData } from '../types' ;
import { isDependencyString , versionLikeSubstring } from '../utils' ;
export const VERSIONS_PROPS = 'versions.props' ;
export const VERSIONS_LOCK = 'versions.lock' ;
const LOCKFILE_HEADER_TEXT =
'# Run ./gradlew --write-locks 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 < string , string | null >
) : boolean {
const versionsLockFile : string = fs . getSiblingFileName (
versionsPropsFilename ,
VERSIONS_LOCK
) ;
return (
fileContents [ versionsLockFile ] ? . startsWith ( LOCKFILE_HEADER_TEXT ) ? ? false
) ;
}
/ * *
* 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 < string , string | null >
) : PackageDependency < GradleManagerData > [ ] {
2023-09-06 13:35:52 +00:00
const propsFileContent = coerceString ( fileContents [ propsFileName ] ) ;
2022-12-08 12:21:14 +00:00
const lockFileName = fs . getSiblingFileName ( propsFileName , VERSIONS_LOCK ) ;
2023-09-06 13:35:52 +00:00
const lockFileContent = coerceString ( fileContents [ lockFileName ] ) ;
2022-12-08 12:21:14 +00:00
const lockFileMap = parseLockFile ( lockFileContent ) ;
const [ propsFileExactMap , propsFileRegexMap ] =
parsePropsFile ( propsFileContent ) ;
const extractedDeps : PackageDependency < GradleManagerData > [ ] = [ ] ;
// For each exact dep in props file
for ( const [ propDep , versionAndPosition ] of propsFileExactMap ) {
if ( lockFileMap . has ( propDep ) ) {
const newDep : Record < string , any > = {
managerData : {
packageFile : propsFileName ,
fileReplacePosition : versionAndPosition.filePos ,
} ,
depName : propDep ,
currentValue : versionAndPosition.version ,
lockedVersion : lockFileMap.get ( propDep ) ? . version ,
depType : lockFileMap.get ( propDep ) ? . depType ,
2023-06-05 19:18:30 +00:00
} satisfies PackageDependency < GradleManagerData > ;
2022-12-08 12:21:14 +00:00
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 < string , any > = {
managerData : {
packageFile : propsFileName ,
fileReplacePosition : propVerAndPos.filePos ,
} ,
depName : exactDep ,
currentValue : propVerAndPos.version ,
lockedVersion : lockVersionAndDepType.version ,
depType : lockVersionAndDepType.depType ,
groupName : propDepGlob ,
2023-06-05 19:18:30 +00:00
} satisfies PackageDependency < GradleManagerData > ;
2022-12-08 12:21:14 +00:00
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 ( /\*/g , '_WC_CHAR_' )
. replace ( /[/\-\\^$*+?.()|[\]{}]/g , '\\$&' )
2023-05-02 07:32:56 +00:00
. replace ( /_WC_CHAR_/g , '.*?' )
2022-12-08 12:21:14 +00:00
) ;
}
interface VersionWithPosition {
version : string ;
filePos : number ;
}
interface VersionWithDepType {
version : string ;
depType : string ;
}
/ * *
* Parses ` versions.lock `
* /
export function parseLockFile ( input : string ) : Map < string , VersionWithDepType > {
const lockLineRegex = regEx (
` ^(?<depName>[^:]+:[^:]+):(?<lockVersion>[^ ]+) \\ ( \\ d+ constraints: [0-9a-f]+ \\ ) $ `
) ;
const depVerMap = new Map < string , VersionWithDepType > ( ) ;
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' ,
} as VersionWithDepType ) ;
}
} 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 < string , VersionWithPosition > , Map < string , VersionWithPosition > ] {
const propsLineRegex = regEx (
` ^(?<depName>[^:]+:[^=]+?) *= *(?<propsVersion>.*) $ `
) ;
const depVerExactMap = new Map < string , VersionWithPosition > ( ) ;
const depVerRegexMap = new Map < string , VersionWithPosition > ( ) ;
let startOfLineIdx = 0 ;
const isCrLf = input . indexOf ( '\r\n' ) > 0 ;
const validGlob = /^[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 ( ) . reverse ( ) ) ] ;
}