File: AppShortcutStringsMetadataCompiler.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 (185 lines) | stat: -rw-r--r-- 9,006 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
178
179
180
181
182
183
184
185
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 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
//
//===----------------------------------------------------------------------===//

public import SWBUtil
import Foundation
import SWBMacro

final public class AppShortcutStringsMetadataCompilerSpec: GenericCommandLineToolSpec, SpecIdentifierType, @unchecked Sendable {
    public static let identifier = "com.apple.compilers.appshortcutstringsmetadata"

    override public func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
        guard cbc.producer.canConstructAppIntentsSSUTask else {
            return
        }

        let stringsFileType = cbc.producer.lookupFileType(identifier: "text.plist.strings")!
        let xcstringsFileType = cbc.producer.lookupFileType(identifier: "text.json.xcstrings")!

        let appShortcutStringsFiles = cbc.inputs.filter({ ($0.fileType.conformsTo(stringsFileType) || $0.fileType.conformsTo(xcstringsFileType)) && ["AppShortcuts.strings", "AppShortcuts.xcstrings"].contains($0.absolutePath.basename) })

        guard appShortcutStringsFiles.count < 2 else {
            assertionFailure("App Shortcuts Validation task construction was passed context with more than one AppShortcut Strings file.")
            return
        }

        let assistantIntentStringsFiles = cbc.inputs.filter({ ($0.fileType.conformsTo(stringsFileType) || $0.fileType.conformsTo(xcstringsFileType)) && ["AssistantIntents.strings", "AssistantIntents.xcstrings"].contains($0.absolutePath.basename) })

        guard assistantIntentStringsFiles.count < 2 else {
            assertionFailure("App Shortcuts Validation task construction was passed context with more than one AssistantIntents Strings file.")
            return
        }

        // We expect either a single AppShortcuts.strings or a single AssistantIntents.strings or both
        guard cbc.inputs.count <= 2 else {
            assertionFailure("App Shortcuts Validation task construction was passed context with too many input files.")
            return
        }

        guard cbc.inputs.count > 0 else {
            assertionFailure("App Shortcuts Validation task construction was passed context with no input files.")
            return
        }

        var inputs: [any PlannedNode] = cbc.inputs.map { delegate.createNode($0.absolutePath) }
        guard let resourcesDir = cbc.resourcesDir else {
            assertionFailure("Resources directory does not exist")
            return
        }
        var metadataDependencyFileListFiles = [String]()
        let inputFilesList = cbc.inputs.map { $0.absolutePath.str }

        let metadataFileListPath = cbc.scope.evaluate(BuiltinMacros.LM_AUX_INTENTS_METADATA_FILES_LIST_PATH)
        if !metadataFileListPath.isEmpty {
            metadataDependencyFileListFiles.append(metadataFileListPath.str)
            inputs.append(delegate.createNode(metadataFileListPath))
        }

        if !cbc.scope.evaluate(BuiltinMacros.BUILD_COMPONENTS).contains("installLoc") {
            // Workaround until we have rdar://93626172 (Re-enable AppIntentsMetadataProcessor outputs)
            let inputOrderingNode = delegate.createVirtualNode("ExtractAppIntentsMetadata \(resourcesDir.join("Metadata.appintents").str )")
            inputs.append(inputOrderingNode)
        }

        let outputNodeIdentifier: String
        let filePathOutputIdentifier = cbc.inputs.map({ $0.absolutePath.str }).joined(separator: " ")
        if let configuredTarget = cbc.producer.configuredTarget {
            outputNodeIdentifier = "ValidateAppShortcutStringsMetadata \(configuredTarget.guid) \(filePathOutputIdentifier)"
        } else {
            outputNodeIdentifier = "ValidateAppShortcutStringsMetadata \(filePathOutputIdentifier)"
        }
        let outputOrderingNode = delegate.createVirtualNode(outputNodeIdentifier)

        func lookup(_ macro: MacroDeclaration) -> MacroExpression? {
            switch macro {
            case BuiltinMacros.LM_STRINGS_FILE_PATH_LIST:
                return cbc.scope.table.namespace.parseLiteralStringList(inputFilesList)
            case BuiltinMacros.LM_INTENTS_METADATA_FILES_LIST_PATH:
                return cbc.scope.table.namespace.parseLiteralStringList(metadataDependencyFileListFiles)
            default:
                return nil
            }
        }

        let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate), lookup: lookup).map(\.asString)
        delegate.createTask(type: self,
                            ruleInfo: defaultRuleInfo(cbc, delegate),
                            commandLine: commandLine,
                            environment: environmentFromSpec(cbc, delegate),
                            workingDirectory: cbc.producer.defaultWorkingDirectory,
                            inputs: inputs,
                            outputs: [outputOrderingNode],
                            action: nil,
                            execDescription: resolveExecutionDescription(cbc, delegate),
                            enableSandboxing: enableSandboxing)
    }

    public override func customOutputParserType(for task: any ExecutableTask) -> (any TaskOutputParser.Type)? {
        return AppShortcutStringsValidationOutputParser.self
    }
}

/// An output parser which forwards all output unchanged, then generates diagnostics from a serialized diagnostics file passed in the payload once it is closed.
public final class AppShortcutStringsValidationOutputParser: TaskOutputParser {
    private let task: any ExecutableTask

    public let workspaceContext: WorkspaceContext
    public let buildRequestContext: BuildRequestContext
    public let delegate: any TaskOutputParserDelegate

    private enum ValidationStatus: String, Codable {
        case success
        case error
        case warning
    }

    private struct ValidationResult: Codable {
        var status: ValidationStatus
        var message: String
        var path: String?
        var line: Int?
        var languageCode: String?
        var key: String?

        var diagnosticLocation: Diagnostic.Location {
            guard let path else { return .unknown }
            if let languageCode,
               let key {
                return .path(Path(path), fileLocation: .object(identifier: "\(languageCode):\(key)"))
            }
            return .path(Path(path), line: line)
        }
    }

    /// The current buffered contents.
    var outputBuffer: [UInt8] = []

    required public init(for task: any ExecutableTask, workspaceContext: WorkspaceContext, buildRequestContext: BuildRequestContext, delegate: any TaskOutputParserDelegate, progressReporter: (any SubtaskProgressReporter)?) {
        self.task = task
        self.workspaceContext = workspaceContext
        self.buildRequestContext = buildRequestContext
        self.delegate = delegate
    }

    public func write(bytes: ByteString) {
        // Keep appending to the buffer to get the full result so that we can read the JSON
        // in close(result: TaskResult?)
        outputBuffer.append(contentsOf: bytes.bytes)
    }

    public func close(result: TaskResult?) {
        defer {
            delegate.close()
        }
        // Don't try to read diagnostics if the process crashed or got cancelled as they were almost certainly not written in this case.
        if result.shouldSkipParsingDiagnostics { return }

        do {
            // TODO: rdar://119739842 (Pass diagnostic file path command line argument to appshortcutstringsvalidator)
            let bytesToParse = outputBuffer.firstRange(of: [UInt8(ascii: "["), UInt8(ascii: "\n")]).map { Array(outputBuffer[$0.startIndex...]) } ?? outputBuffer
            let validationResult: [ValidationResult] = try JSONDecoder().decode([ValidationResult].self, from: Data(bytesToParse))

            for result in validationResult {
                switch result.status {
                case .success:
                    continue
                case .warning:
                    delegate.diagnosticsEngine.emit(Diagnostic(behavior: .warning, location: result.diagnosticLocation, data: DiagnosticData(result.message)))
                case .error:
                    delegate.diagnosticsEngine.emit(Diagnostic(behavior: .error, location: result.diagnosticLocation, data: DiagnosticData(result.message)))
                }
            }
        } catch {
            delegate.diagnosticsEngine.emit(data: DiagnosticData("Unable to parse diagnostics: \(error.localizedDescription)"), behavior: .warning)
        }
    }
}