From ed9ce1144ef1dc69a497c50bedb41798fc6a020d Mon Sep 17 00:00:00 2001 From: Frank Viernau Date: Tue, 16 Jul 2024 13:30:31 +0200 Subject: [PATCH] refactor(cocoapods): Separate parsing the lockfile Previously, parsing the lockfile and mapping it to ORT's model has all been done in the single function `parseLockfile(File)`. Define a model for the lockfile and a function for its parsing in a separate dedicated file in order to separate concerns. This makes the code easier to maintain and simplifies the upcoming porting to KxS. Signed-off-by: Frank Viernau --- .../cocoapods/src/main/kotlin/CocoaPods.kt | 51 +++------ .../cocoapods/src/main/kotlin/Lockfile.kt | 100 ++++++++++++++++++ 2 files changed, 114 insertions(+), 37 deletions(-) create mode 100644 plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt diff --git a/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt b/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt index cb63226fe21a0..d1a073f9f358f 100644 --- a/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt +++ b/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt @@ -19,8 +19,6 @@ package org.ossreviewtoolkit.plugins.packagemanagers.cocoapods -import com.fasterxml.jackson.databind.node.ObjectNode - import java.io.File import java.io.IOException @@ -45,12 +43,10 @@ import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.createAndLogIssue import org.ossreviewtoolkit.model.orEmpty import org.ossreviewtoolkit.model.utils.toPurl -import org.ossreviewtoolkit.model.yamlMapper import org.ossreviewtoolkit.utils.common.CommandLineTool import org.ossreviewtoolkit.utils.common.Os import org.ossreviewtoolkit.utils.common.collectMessages import org.ossreviewtoolkit.utils.common.stashDirectories -import org.ossreviewtoolkit.utils.common.textValueOrEmpty import org.semver4j.RangesList import org.semver4j.RangesListFactory @@ -223,50 +219,32 @@ private const val LOCKFILE_FILENAME = "Podfile.lock" private const val SCOPE_NAME = "dependencies" -private fun parseNameAndVersion(entry: String): Pair { - val info = entry.split(' ', limit = 2) - val name = info[0] - - // A version entry could look something like "(6.3.0)", "(= 2021.06.28.00-v2)", "(~> 8.15.0)", etc. Also see - // https://guides.cocoapods.org/syntax/podfile.html#pod. - val version = info.getOrNull(1)?.removeSurrounding("(", ")") - - return name to version -} - private data class LockfileData( val dependencies: Set, val externalSources: Map ) private fun parseLockfile(podfileLock: File): LockfileData { + val lockfile = podfileLock.readText().parseLockfile() val resolvedVersions = mutableMapOf() val dependencyConstraints = mutableMapOf>() - val root = yamlMapper.readTree(podfileLock) // The "PODS" section lists the resolved dependencies and, nested by one level, any version constraints of their // direct dependencies. That is, the nesting never goes deeper than two levels. - root.get("PODS").forEach { node -> - when (node) { - is ObjectNode -> { - val (name, version) = parseNameAndVersion(node.fieldNames().asSequence().single()) - resolvedVersions[name] = checkNotNull(version) - dependencyConstraints[name] = node.single().mapTo(mutableSetOf()) { - // Discard the version (which is only a constraint in this case) and just take the name. - parseNameAndVersion(it.textValue()).first - } - } + lockfile.pods.map { pod -> + resolvedVersions[pod.name] = checkNotNull(pod.version) - else -> { - val (name, version) = parseNameAndVersion(node.textValue()) - resolvedVersions[name] = checkNotNull(version) + if (pod.dependencies.isNotEmpty()) { + dependencyConstraints[pod.name] = pod.dependencies.mapTo(mutableSetOf()) { + // Discard the version (which is only a constraint in this case) and just take the name. + it.name } } } - val externalSources = root.get("CHECKOUT OPTIONS")?.fields()?.asSequence()?.mapNotNull { (name, checkout) -> - val url = checkout[":git"]?.textValue() ?: return@mapNotNull null - val revision = checkout[":commit"].textValueOrEmpty() + val externalSources = lockfile.checkoutOptions.mapNotNull { (name, checkoutOption) -> + val url = checkoutOption.git ?: return@mapNotNull null + val revision = checkoutOption.commit.orEmpty() // The version written to the lockfile matches the version specified in the project's ".podspec" file at the // given revision, so the same version might be used in different revisions. To still get a unique identifier, @@ -287,7 +265,7 @@ private fun parseLockfile(podfileLock: File): LockfileData { sourceArtifact = RemoteArtifact.EMPTY, vcs = VcsInfo(VcsType.GIT, url, revision) ) - }.orEmpty().toMap() + }.toMap() fun createPackageReference(name: String): PackageReference = PackageReference( @@ -302,10 +280,9 @@ private fun parseLockfile(podfileLock: File): LockfileData { // The "DEPENDENCIES" section lists direct dependencies, but only along with version constraints, not with their // resolved versions, and eventually additional information about the source. - val dependencies = root.get("DEPENDENCIES").mapTo(mutableSetOf()) { node -> - // Discard the version (which is only a constraint in this case) and just take the name. - val (name, _) = parseNameAndVersion(node.textValue()) - createPackageReference(name) + val dependencies = lockfile.dependencies.mapTo(mutableSetOf()) { dependency -> + // Ignore the version (which is only a constraint in this case) and just take the name. + createPackageReference(dependency.name) } return LockfileData(dependencies, externalSources) diff --git a/plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt b/plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt new file mode 100644 index 0000000000000..46a057bd1c41f --- /dev/null +++ b/plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.packagemanagers.cocoapods + +import com.fasterxml.jackson.databind.node.ObjectNode + +import org.ossreviewtoolkit.model.yamlMapper +import org.ossreviewtoolkit.plugins.packagemanagers.cocoapods.Lockfile.CheckoutOption +import org.ossreviewtoolkit.plugins.packagemanagers.cocoapods.Lockfile.Dependency + +internal data class Lockfile( + val pods: List, + val checkoutOptions: Map, + val dependencies: List +) { + data class Pod( + val name: String, + val version: String? = null, + val dependencies: List = emptyList() + ) + + data class CheckoutOption( + val git: String?, + val commit: String? + ) + + data class Dependency( + val name: String, + val versionConstraint: String? + ) +} + +internal fun String.parseLockfile(): Lockfile { + val root = yamlMapper.readTree(this) + + val pods = root.get("PODS").map { node -> + when (node) { + is ObjectNode -> { + val (name, version) = parseNameAndVersion(node.fieldNames().asSequence().single()) + Lockfile.Pod( + name = name, + version = version, + dependencies = node.single().map { + val (depName, depVersion) = parseNameAndVersion(it.textValue()) + Lockfile.Pod(depName, depVersion) + } + ) + } + + else -> { + val (name, version) = parseNameAndVersion(node.textValue()) + Lockfile.Pod(name, version) + } + } + } + + val checkoutOptions = root.get("CHECKOUT OPTIONS")?.fields()?.asSequence()?.mapNotNull { (name, node) -> + val checkoutOption = CheckoutOption( + git = node[":git"]?.textValue(), + commit = node[":commit"].textValue() + ) + + name to checkoutOption + }.orEmpty().toMap() + + val dependencies = root.get("DEPENDENCIES").map { node -> + val (name, version) = parseNameAndVersion(node.textValue()) + Dependency(name, version) + } + + return Lockfile(pods, checkoutOptions, dependencies) +} + +private fun parseNameAndVersion(entry: String): Pair { + val info = entry.split(' ', limit = 2) + val name = info[0] + + // A version entry could look something like "(6.3.0)", "(= 2021.06.28.00-v2)", "(~> 8.15.0)", etc. Also see + // https://guides.cocoapods.org/syntax/podfile.html#pod. + val version = info.getOrNull(1)?.removeSurrounding("(", ")") + + return name to version +}