File: SourceKitDRequestExecutor.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (177 lines) | stat: -rw-r--r-- 6,080 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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import SourceKitD

import struct TSCBasic.AbsolutePath
import class TSCBasic.Process
import struct TSCBasic.ProcessResult

/// The different states in which a sourcekitd request can finish.
@_spi(Testing)
public enum SourceKitDRequestResult: Sendable {
  /// The request succeeded.
  case success(response: String)

  /// The request failed but did not crash.
  case error

  /// Running the request reproduces the issue that should be reduced.
  case reproducesIssue
}

fileprivate extension String {
  init?(bytes: [UInt8], encoding: Encoding) {
    self = bytes.withUnsafeBytes { buffer in
      guard let baseAddress = buffer.baseAddress else {
        return ""
      }
      let data = Data(bytes: baseAddress, count: buffer.count)
      return String(data: data, encoding: encoding)!
    }

  }
}

/// An executor that can run a sourcekitd request and indicate whether the request reprodes a specified issue.
@_spi(Testing)
public protocol SourceKitRequestExecutor {
  @MainActor func runSourceKitD(request: RequestInfo) async throws -> SourceKitDRequestResult
  @MainActor func runSwiftFrontend(request: RequestInfo) async throws -> SourceKitDRequestResult
}

extension SourceKitRequestExecutor {
  @MainActor
  func run(request: RequestInfo) async throws -> SourceKitDRequestResult {
    if request.requestTemplate == RequestInfo.fakeRequestTemplateForFrontendIssues {
      return try await runSwiftFrontend(request: request)
    } else {
      return try await runSourceKitD(request: request)
    }
  }
}

/// Runs `sourcekit-lsp run-sourcekitd-request` to check if a sourcekit-request crashes.
@_spi(Testing)
public class OutOfProcessSourceKitRequestExecutor: SourceKitRequestExecutor {
  /// The path to `sourcekitd.framework/sourcekitd`.
  private let sourcekitd: URL

  /// The path to `swift-frontend`.
  private let swiftFrontend: URL

  /// The file to which we write the reduce source file.
  private let temporarySourceFile: URL

  /// The file to which we write the YAML request that we want to run.
  private let temporaryRequestFile: URL

  /// If this predicate evaluates to true on the sourcekitd response, the request is
  /// considered to reproduce the issue.
  private let reproducerPredicate: NSPredicate?

  @_spi(Testing)
  public init(sourcekitd: URL, swiftFrontend: URL, reproducerPredicate: NSPredicate?) {
    self.sourcekitd = sourcekitd
    self.swiftFrontend = swiftFrontend
    self.reproducerPredicate = reproducerPredicate
    temporaryRequestFile = FileManager.default.temporaryDirectory.appendingPathComponent("request-\(UUID()).yml")
    temporarySourceFile = FileManager.default.temporaryDirectory.appendingPathComponent("recude-\(UUID()).swift")
  }

  deinit {
    try? FileManager.default.removeItem(at: temporaryRequestFile)
    try? FileManager.default.removeItem(at: temporarySourceFile)
  }

  /// The `SourceKitDRequestResult` for the given process result, evaluating the reproducer predicate, if it was
  /// specified.
  private func requestResult(for result: ProcessResult) -> SourceKitDRequestResult {
    if let reproducerPredicate {
      if let outputStr = try? String(bytes: result.output.get(), encoding: .utf8),
        let stderrStr = try? String(bytes: result.stderrOutput.get(), encoding: .utf8)
      {
        let exitCode: Int32? =
          switch result.exitStatus {
          case .terminated(code: let exitCode): exitCode
          default: nil
          }

        let dict: [String: Any] = [
          "stdout": outputStr,
          "stderr": stderrStr,
          "exitCode": exitCode as Any,
        ]

        if reproducerPredicate.evaluate(with: dict) {
          return .reproducesIssue
        } else {
          return .error
        }
      } else {
        return .error
      }
    }

    switch result.exitStatus {
    case .terminated(code: 0):
      if let outputStr = try? String(bytes: result.output.get(), encoding: .utf8) {
        return .success(response: outputStr)
      } else {
        return .error
      }
    case .terminated(code: 1):
      // The request failed but did not crash. It doesn't reproduce the issue.
      return .error
    default:
      // Exited with a non-zero and non-one exit code. Looks like it crashed, so reproduces a crasher.
      return .reproducesIssue
    }
  }

  @_spi(Testing)
  public func runSwiftFrontend(request: RequestInfo) async throws -> SourceKitDRequestResult {
    try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8)

    let arguments = request.compilerArgs.replacing(["$FILE"], with: [temporarySourceFile.path])

    let process = Process(arguments: [swiftFrontend.path] + arguments)
    try process.launch()
    let result = try await process.waitUntilExit()

    return requestResult(for: result)
  }

  @_spi(Testing)
  public func runSourceKitD(request: RequestInfo) async throws -> SourceKitDRequestResult {
    try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8)
    let requestString = try request.request(for: temporarySourceFile)
    try requestString.write(to: temporaryRequestFile, atomically: true, encoding: .utf8)

    let process = Process(
      arguments: [
        ProcessInfo.processInfo.arguments[0],
        "debug",
        "run-sourcekitd-request",
        "--sourcekitd",
        sourcekitd.path,
        "--request-file",
        temporaryRequestFile.path,
      ]
    )
    try process.launch()
    let result = try await process.waitUntilExit()

    return requestResult(for: result)
  }
}