Skip to content

Commit

Permalink
refactor(stack): Move several function to the top level
Browse files Browse the repository at this point in the history
Signed-off-by: Frank Viernau <frank_viernau@epam.com>
  • Loading branch information
fviernau committed Jul 3, 2024
1 parent f8fc96c commit f81750a
Showing 1 changed file with 136 additions and 133 deletions.
269 changes: 136 additions & 133 deletions plugins/package-managers/stack/src/main/kotlin/Stack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,7 +171,7 @@ class Stack(
)

if (location == null || location.type == HACKAGE_PACKAGE_TYPE) {
downloadCabalFile(id)?.let { return parseCabalFile(it, "Hackage") }
okHttpClient.downloadCabalFile(id)?.let { return parseCabalFile(it, "Hackage") }
}

return Package.EMPTY.copy(
Expand All @@ -177,167 +180,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<Dependency>.toScope(scopeName: String, packageForName: Map<String, Package>): 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<String>, keyPrefix: String = ""): Map<String, String> {
fun getIndentation(line: String) = line.takeWhile { it.isWhitespace() }.length
private fun Dependency.toPackageReference(
dependencyForName: Map<String, Dependency>,
packageForName: Map<String, Package>
): 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<String, String>()
private fun parseKeyValue(i: ListIterator<String>, keyPrefix: String = ""): Map<String, String> {
fun getIndentation(line: String) = line.takeWhile { it.isWhitespace() }.length

while (i.hasNext()) {
val line = i.next()
var indentation: Int? = null
val map = mutableMapOf<String, String>()

// 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<String>()
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<String>()

// 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<Dependency>.toScope(scopeName: String, packageForName: Map<String, Package>): 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<String, Dependency>,
packageForName: Map<String, Package>
): PackageReference =
PackageReference(
id = packageForName.getValue(name).id,
dependencies = dependencies.mapTo(mutableSetOf()) { name ->
dependencyForName.getValue(name).toPackageReference(dependencyForName, packageForName)
}
)

0 comments on commit f81750a

Please sign in to comment.