NpmPackageParser.kt

package com.depanalyzer.parser.npm

import com.depanalyzer.parser.DependencySection
import com.depanalyzer.parser.Ecosystem
import com.depanalyzer.parser.ParsedDependency
import tools.jackson.databind.JsonNode
import tools.jackson.databind.json.JsonMapper
import java.io.File

class NpmPackageParser {
    private val jsonMapper = JsonMapper.builder().build()

    fun parse(packageJsonFile: File): List<ParsedDependency> {
        require(packageJsonFile.exists() && packageJsonFile.isFile) {
            "Invalid package.json path: ${packageJsonFile.absolutePath}"
        }
        require(packageJsonFile.name == "package.json") { "Expected package.json, got ${packageJsonFile.name}" }

        val root = jsonMapper.readTree(packageJsonFile)
        val result = mutableListOf<ParsedDependency>()

        result += parseSection(root.path("dependencies"), "dependencies")
        result += parseSection(root.path("devDependencies"), "devDependencies")
        result += parseSection(root.path("peerDependencies"), "peerDependencies")
        result += parseSection(root.path("optionalDependencies"), "optionalDependencies")

        return result.distinctBy { "${it.groupId}:${it.artifactId}" }
    }

    private fun parseSection(sectionNode: JsonNode, scope: String): List<ParsedDependency> {
        if (!sectionNode.isObject) return emptyList()

        return sectionNode.properties().asSequence().mapNotNull { entry ->
            val packageName = entry.key.trim()
            if (packageName.isBlank()) return@mapNotNull null

            val versionSpec = entry.value.textOrEmpty().takeIf { it.isNotEmpty() }
            val (groupId, artifactId) = packageName.toGroupArtifact()

            ParsedDependency(
                groupId = groupId,
                artifactId = artifactId,
                version = versionSpec,
                scope = scope,
                section = DependencySection.DEPENDENCIES,
                ecosystem = Ecosystem.NPM
            )
        }.toList()
    }

    private fun String.toGroupArtifact(): Pair<String, String> {
        return if (startsWith("@") && contains('/')) {
            substringBefore('/') to substringAfter('/')
        } else {
            "npm" to this
        }
    }

    private fun JsonNode.textOrEmpty(): String = scalarText().trim()

    private fun JsonNode.scalarText(): String = when {
        isNull || isMissingNode -> ""
        else -> toString().removeSurrounding("\"")
    }
}