File: AddPackageDependency.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (133 lines) | stat: -rw-r--r-- 5,311 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Basics
import PackageLoading
import PackageModel
import SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder

/// Add a package dependency to a manifest's source code.
public enum AddPackageDependency {
    /// The set of argument labels that can occur after the "dependencies"
    /// argument in the Package initializers.
    ///
    /// TODO: Could we generate this from the the PackageDescription module, so
    /// we don't have keep it up-to-date manually?
    private static let argumentLabelsAfterDependencies: Set<String> = [
        "targets",
        "swiftLanguageVersions",
        "cLanguageStandard",
        "cxxLanguageStandard",
    ]

    /// Produce the set of source edits needed to add the given package
    /// dependency to the given manifest file.
    public static func addPackageDependency(
        _ dependency: MappablePackageDependency.Kind,
        to manifest: SourceFileSyntax
    ) throws -> PackageEditResult {
        // Make sure we have a suitable tools version in the manifest.
        try manifest.checkEditManifestToolsVersion()

        guard let packageCall = manifest.findCall(calleeName: "Package") else {
            throw ManifestEditError.cannotFindPackage
        }

        guard try !dependencyAlreadyAdded(
            dependency,
            in: packageCall
        ) else {
            return PackageEditResult(manifestEdits: [])
        }

        let newPackageCall = try addPackageDependencyLocal(
            dependency, to: packageCall
        )

        return PackageEditResult(
            manifestEdits: [
                .replace(packageCall, with: newPackageCall.description),
            ]
        )
    }

    /// Return `true` if the dependency already exists in the manifest, otherwise return `false`.
    /// Throws an error if a dependency already exists with the same id or url, but different arguments.
    private static func dependencyAlreadyAdded(
        _ dependency: MappablePackageDependency.Kind,
        in packageCall: FunctionCallExprSyntax
    ) throws -> Bool {
        let dependencySyntax = dependency.asSyntax()
        guard let dependenctFnSyntax = dependencySyntax.as(FunctionCallExprSyntax.self) else {
            throw ManifestEditError.cannotFindPackage
        }

        guard let id = dependenctFnSyntax.arguments.first(where: {
            $0.label?.text == "url" || $0.label?.text == "id" || $0.label?.text == "path"
        }) else {
            throw InternalError("Missing id or url argument in dependency syntax")
        }

        if let existingDependencies = packageCall.findArgument(labeled: "dependencies") {
            // If we have an existing dependencies array, we need to check if
            if let expr = existingDependencies.expression.as(ArrayExprSyntax.self) {
                // Iterate through existing dependencies and look for an argument that matches
                // either the `id` or `url` argument of the new dependency. 
                let existingArgument = expr.elements.first { elem in
                    if let funcExpr = elem.expression.as(FunctionCallExprSyntax.self) {
                        return funcExpr.arguments.contains {
                            $0.trimmedDescription == id.trimmedDescription
                        }
                    }
                    return true
                }

                if let existingArgument {
                    let normalizedExistingArgument = existingArgument.detached.with(\.trailingComma, nil)
                    // This exact dependency already exists, return false to indicate we should do nothing.
                    if normalizedExistingArgument.trimmedDescription == dependencySyntax.trimmedDescription {
                        return true
                    }
                    throw ManifestEditError.existingDependency(dependencyName: dependency.identifier)
                }
            }
        }
        return false
    }

    /// Implementation of adding a package dependency to an existing call.
    static func addPackageDependencyLocal(
        _ dependency: MappablePackageDependency.Kind,
        to packageCall: FunctionCallExprSyntax
    ) throws -> FunctionCallExprSyntax {
        try packageCall.appendingToArrayArgument(
            label: "dependencies",
            trailingLabels: self.argumentLabelsAfterDependencies,
            newElement: dependency.asSyntax()
        )
    }
}

fileprivate extension MappablePackageDependency.Kind {
    var identifier: String {
        switch self {
            case .sourceControl(let name, let path, _):
                return name ?? path
            case .fileSystem(let name, let location):
                return name ?? location
            case .registry(let id, _):
                return id
        }
    }
}