From 1c19b2c770c13c48bbf28d18b9c5b755fd314150 Mon Sep 17 00:00:00 2001 From: Frank Viernau Date: Tue, 2 Jul 2024 16:01:06 +0200 Subject: [PATCH] refactor(stack): Move several function to the top level Signed-off-by: Frank Viernau --- .../stack/src/main/kotlin/Stack.kt | 269 +++++++++--------- 1 file changed, 136 insertions(+), 133 deletions(-) diff --git a/plugins/package-managers/stack/src/main/kotlin/Stack.kt b/plugins/package-managers/stack/src/main/kotlin/Stack.kt index 7e76f4947a025..195d46de3a61b 100644 --- a/plugins/package-managers/stack/src/main/kotlin/Stack.kt +++ b/plugins/package-managers/stack/src/main/kotlin/Stack.kt @@ -22,10 +22,13 @@ package org.ossreviewtoolkit.plugins.packagemanagers.stack import java.io.File import java.io.IOException +import okhttp3.OkHttpClient + import org.apache.logging.log4j.kotlin.logger import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory import org.ossreviewtoolkit.analyzer.PackageManager +import org.ossreviewtoolkit.analyzer.PackageManager.Companion.processPackageVcs import org.ossreviewtoolkit.analyzer.parseAuthorString import org.ossreviewtoolkit.downloader.VersionControlSystem import org.ossreviewtoolkit.model.Identifier @@ -171,7 +174,7 @@ class Stack( ) if (location?.type in setOf(null, HACKAGE_PACKAGE_TYPE)) { - downloadCabalFile(id)?.let { return parseCabalFile(it, "Hackage") } + okHttpClient.downloadCabalFile(id)?.let { return parseCabalFile(it, "Hackage") } } return Package.EMPTY.copy( @@ -180,167 +183,167 @@ class Stack( declaredLicenses = setOf(license) ) } +} - private fun getPackageUrl(name: String, version: String) = "https://hackage.haskell.org/package/$name-$version" +private fun Dependency.isProject(): Boolean = location?.type == PROJECT_PACKAGE_TYPE - private fun downloadCabalFile(pkgId: Identifier): String? { - val url = "${getPackageUrl(pkgId.name, pkgId.version)}/src/${pkgId.name}.cabal" +private fun Collection.toScope(scopeName: String, packageForName: Map): Scope { + // TODO: Stack identifies dependencies only by name. Find out how dependencies with the same name but in + // different namespaces should be handled. + val dependencyForName = associateBy { it.name } - return okHttpClient.downloadText(url).onFailure { - logger.warn { "Unable to retrieve Hackage metadata for package '${pkgId.toCoordinates()}'." } - }.getOrNull() - } + return Scope( + name = scopeName, + dependencies = single { + it.location?.type == PROJECT_PACKAGE_TYPE + }.dependencies.mapTo(mutableSetOf()) { name -> + dependencyForName.getValue(name).toPackageReference(dependencyForName, packageForName) + } + ) +} - private fun parseKeyValue(i: ListIterator, keyPrefix: String = ""): Map { - fun getIndentation(line: String) = line.takeWhile { it.isWhitespace() }.length +private fun Dependency.toPackageReference( + dependencyForName: Map, + packageForName: Map +): PackageReference = + PackageReference( + id = packageForName.getValue(name).id, + dependencies = dependencies.mapTo(mutableSetOf()) { name -> + dependencyForName.getValue(name).toPackageReference(dependencyForName, packageForName) + } + ) - var indentation: Int? = null - val map = mutableMapOf() +private fun parseKeyValue(i: ListIterator, keyPrefix: String = ""): Map { + fun getIndentation(line: String) = line.takeWhile { it.isWhitespace() }.length - while (i.hasNext()) { - val line = i.next() + var indentation: Int? = null + val map = mutableMapOf() - // Skip blank lines and comments. - if (line.isBlank() || line.trimStart().startsWith("--")) continue + while (i.hasNext()) { + val line = i.next() - if (indentation == null) { - indentation = getIndentation(line) - } else if (indentation != getIndentation(line)) { - // Stop if the indentation level changes. - i.previous() - break - } + // Skip blank lines and comments. + if (line.isBlank() || line.trimStart().startsWith("--")) continue - val keyValue = line.split(':', limit = 2).map { it.trim() } - when (keyValue.size) { - 1 -> { - // Handle lines without a colon. - val nestedMap = parseKeyValue(i, keyPrefix + keyValue[0].replace(" ", "-") + "-") - map += nestedMap - } + if (indentation == null) { + indentation = getIndentation(line) + } else if (indentation != getIndentation(line)) { + // Stop if the indentation level changes. + i.previous() + break + } - 2 -> { - // Handle lines with a colon. - val key = (keyPrefix + keyValue[0]).lowercase() + val keyValue = line.split(':', limit = 2).map { it.trim() } + when (keyValue.size) { + 1 -> { + // Handle lines without a colon. + val nestedMap = parseKeyValue(i, keyPrefix + keyValue[0].replace(" ", "-") + "-") + map += nestedMap + } - val valueLines = mutableListOf() + 2 -> { + // Handle lines with a colon. + val key = (keyPrefix + keyValue[0]).lowercase() - var isBlock = false - if (keyValue[1].isNotEmpty()) { - if (keyValue[1] == "{") { - // Support multi-line values that use curly braces instead of indentation. - isBlock = true - } else { - valueLines += keyValue[1] - } - } + val valueLines = mutableListOf() - // Parse a multi-line value. - while (i.hasNext()) { - var indentedLine = i.next() - - if (isBlock) { - if (indentedLine == "}") { - // Stop if a block closes. - break - } - } else { - if (indentedLine.isNotBlank() && getIndentation(indentedLine) <= indentation) { - // Stop if the indentation level does not increase. - i.previous() - break - } - } + var isBlock = false + if (keyValue[1].isNotEmpty()) { + if (keyValue[1] == "{") { + // Support multi-line values that use curly braces instead of indentation. + isBlock = true + } else { + valueLines += keyValue[1] + } + } - indentedLine = indentedLine.trim() + // Parse a multi-line value. + while (i.hasNext()) { + var indentedLine = i.next() - // Within a multi-line value, lines with only a dot mark empty lines. - if (indentedLine == ".") { - if (valueLines.isNotEmpty()) valueLines += "" - } else { - valueLines += indentedLine + if (isBlock) { + if (indentedLine == "}") { + // Stop if a block closes. + break + } + } else { + if (indentedLine.isNotBlank() && getIndentation(indentedLine) <= indentation) { + // Stop if the indentation level does not increase. + i.previous() + break } } - val trimmedValueLines = valueLines.dropWhile { it.isBlank() }.dropLastWhile { it.isBlank() } - map[key] = trimmedValueLines.joinToString("\n") + indentedLine = indentedLine.trim() + + // Within a multi-line value, lines with only a dot mark empty lines. + if (indentedLine == ".") { + if (valueLines.isNotEmpty()) valueLines += "" + } else { + valueLines += indentedLine + } } + + val trimmedValueLines = valueLines.dropWhile { it.isBlank() }.dropLastWhile { it.isBlank() } + map[key] = trimmedValueLines.joinToString("\n") } } - - return map } - // TODO: Consider replacing this with a Haskell helper script that calls "readGenericPackageDescription" and dumps - // it as JSON to the console. - private fun parseCabalFile(cabal: String, identifierType: String): Package { - // For an example file see - // https://hackage.haskell.org/package/transformers-compat-0.5.1.4/src/transformers-compat.cabal - val map = parseKeyValue(cabal.lines().listIterator()) - - val id = Identifier( - type = identifierType, - namespace = map["category"].orEmpty(), - name = map["name"].orEmpty(), - version = map["version"].orEmpty() - ) + return map +} - val artifact = RemoteArtifact.EMPTY.copy( - url = "${getPackageUrl(id.name, id.version)}/${id.name}-${id.version}.tar.gz" - ) +// TODO: Consider replacing this with a Haskell helper script that calls "readGenericPackageDescription" and dumps +// it as JSON to the console. +private fun parseCabalFile(cabal: String, identifierType: String): Package { + // For an example file see + // https://hackage.haskell.org/package/transformers-compat-0.5.1.4/src/transformers-compat.cabal + val map = parseKeyValue(cabal.lines().listIterator()) + + val id = Identifier( + type = identifierType, + namespace = map["category"].orEmpty(), + name = map["name"].orEmpty(), + version = map["version"].orEmpty() + ) - val vcsType = (map["source-repository-this-type"] ?: map["source-repository-head-type"]).orEmpty() - val vcsUrl = (map["source-repository-this-location"] ?: map["source-repository-head-location"]).orEmpty() - val vcs = VcsInfo( - type = VcsType.forName(vcsType), - revision = map["source-repository-this-tag"].orEmpty(), - url = vcsUrl - ) + val artifact = RemoteArtifact.EMPTY.copy( + url = "${getPackageUrl(id.name, id.version)}/${id.name}-${id.version}.tar.gz" + ) - val homepageUrl = map["homepage"].orEmpty() + val vcsType = (map["source-repository-this-type"] ?: map["source-repository-head-type"]).orEmpty() + val vcsUrl = (map["source-repository-this-location"] ?: map["source-repository-head-location"]).orEmpty() + val vcs = VcsInfo( + type = VcsType.forName(vcsType), + revision = map["source-repository-this-tag"].orEmpty(), + url = vcsUrl + ) - return Package( - id = id, - authors = map["author"].orEmpty() - .split(',') - .map(String::trim) - .filter(String::isNotEmpty) - .mapNotNullTo(mutableSetOf(), ::parseAuthorString), - declaredLicenses = setOfNotNull(map["license"]), - description = map["description"].orEmpty(), - homepageUrl = homepageUrl, - binaryArtifact = RemoteArtifact.EMPTY, - sourceArtifact = artifact, - vcs = vcs, - vcsProcessed = processPackageVcs(vcs, homepageUrl) - ) - } + val homepageUrl = map["homepage"].orEmpty() + + return Package( + id = id, + authors = map["author"].orEmpty() + .split(',') + .map(String::trim) + .filter(String::isNotEmpty) + .mapNotNullTo(mutableSetOf(), ::parseAuthorString), + declaredLicenses = setOfNotNull(map["license"]), + description = map["description"].orEmpty(), + homepageUrl = homepageUrl, + binaryArtifact = RemoteArtifact.EMPTY, + sourceArtifact = artifact, + vcs = vcs, + vcsProcessed = processPackageVcs(vcs, homepageUrl) + ) } -private fun Dependency.isProject(): Boolean = location?.type == PROJECT_PACKAGE_TYPE +private fun getPackageUrl(name: String, version: String) = "https://hackage.haskell.org/package/$name-$version" -private fun Collection.toScope(scopeName: String, packageForName: Map): Scope { - // TODO: Stack identifies dependencies only by name. Find out how dependencies with the same name but in - // different namespaces should be handled. - val dependencyForName = associateBy { it.name } +private fun OkHttpClient.downloadCabalFile(pkgId: Identifier): String? { + val url = "${getPackageUrl(pkgId.name, pkgId.version)}/src/${pkgId.name}.cabal" - return Scope( - name = scopeName, - dependencies = single { - it.location?.type == PROJECT_PACKAGE_TYPE - }.dependencies.mapTo(mutableSetOf()) { name -> - dependencyForName.getValue(name).toPackageReference(dependencyForName, packageForName) - } - ) + return okHttpClient.downloadText(url).onFailure { + logger.warn { "Unable to retrieve Hackage metadata for package '${pkgId.toCoordinates()}'." } + }.getOrNull() } - -private fun Dependency.toPackageReference( - dependencyForName: Map, - packageForName: Map -): PackageReference = - PackageReference( - id = packageForName.getValue(name).id, - dependencies = dependencies.mapTo(mutableSetOf()) { name -> - dependencyForName.getValue(name).toPackageReference(dependencyForName, packageForName) - } - )