Files
Andrew Heard bc9f9ea1da feat(ai): add FoundationModelsFake for GLM tests (#16995)
Add FoundationModelsFake, a nested Swift package that fakes Apple's
Foundation Models framework, so that GeminiLanguageModel (GLM) can build
and run its unit tests on Linux and on OS versions without the real
framework. It's used only for development and testing.

- GLM depends on the fake through a `FoundationModelsFake` package
  trait, which is on by default where Darwin isn't available (e.g.,
  Linux). With the trait off, the fake isn't built. The root
  `Package.swift` and podspecs can't reach it.
- The fake mirrors Apple's public API and availability in the 27.2
  SDKs, including `@Generable` macro expansions. It omits
  PrivateCloudComputeLanguageModel, OCRTool, BarcodeReaderTool, the
  adapter and feedback APIs, and deprecated APIs.
  SystemLanguageModel is always unavailable. Below Swift 6.4 it
  compiles to an empty module.
- Decoding rejects transcript images with `file:` URLs and schemas
  whose references never resolve, instead of reading local files or
  overflowing the stack.
- Without ImageIO, GLM passes PNG, JPEG, WebP, HEIC, and HEIF images
  through to Gemini after checking their signatures, and rejects
  images that would need re-encoding.
- Add CI jobs that run both packages' tests against the fake: one on
  Linux in a digest-pinned `swift:6.4.2` container, and one on macOS
  with Xcode 27.2. Skip `sdk.ai` for changes that only touch the fake.
2026-11-05 14:13:50 -05:00

251 lines
9.0 KiB
Swift

// Copyright 2026 Google LLC
//
// 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
//
// http://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.
import SwiftDiagnostics
import SwiftSyntax
/// A stored property (or enum payload) that becomes a property of a generated schema.
struct GenerableProperty {
/// The property name used in the schema and in generated content, without backticks.
let schemaName: String
/// The property name as written in source, including any backticks.
let identifier: String
/// The declared type of the property.
let type: TypeSyntax
/// The `description:` argument of the property's `@Guide`, if any.
var description: ExprSyntax?
/// The unlabeled arguments of the property's `@Guide` attributes.
var guides: [ExprSyntax]
/// The property's declaration modifiers as written (such as `public private(set)`), each
/// followed by a space, or an empty string.
///
/// Like Apple's macro, the matching `PartiallyGenerated` property repeats them.
var modifierPrefix: String
/// Creates a property.
///
/// - Parameters:
/// - name: The property name as written in source.
/// - type: The declared type.
/// - description: The guide description.
/// - guides: The guide expressions.
/// - modifiers: The declaration modifiers of the property.
init(
name: TokenSyntax,
type: TypeSyntax,
description: ExprSyntax? = nil,
guides: [ExprSyntax] = [],
modifiers: DeclModifierListSyntax = []
) {
self.schemaName = name.unescapedText
self.identifier = name.trimmedDescription
self.type = type.normalizedForGeneration
self.description = description
self.guides = guides
self.modifierPrefix = modifiers.map { "\($0.trimmedDescription) " }.joined()
}
/// Creates a property with an explicit schema name and identifier.
///
/// - Parameters:
/// - schemaName: The name used in the schema.
/// - identifier: The name used in source.
/// - type: The declared type.
/// - guides: The guide expressions.
init(schemaName: String, identifier: String, type: TypeSyntax, guides: [ExprSyntax] = []) {
self.schemaName = schemaName
self.identifier = identifier
self.type = type.normalizedForGeneration
self.description = nil
self.guides = guides
self.modifierPrefix = ""
}
/// Whether this is an `id: GenerationID` or `id: GenerationID?` property, which maps to the
/// content's generation ID instead of a schema property.
var isGenerationID: Bool {
schemaName == "id" && generationIDOptionality != nil
}
/// Whether the type is `GenerationID` (`false`) or an optional `GenerationID` (`true`), or `nil`
/// if it's neither.
private var generationIDOptionality: Bool? {
let typeName = type.trimmedDescription.filter { !$0.isWhitespace }
let names = ["GenerationID", "\(moduleName).GenerationID"]
if names.contains(typeName) {
return false
}
let optionalForms = names.flatMap { ["\($0)?", "Optional<\($0)>", "Swift.Optional<\($0)>"] }
return optionalForms.contains(typeName) ? true : nil
}
/// The `GenerationSchema.Property(...)` expression for this property.
var schemaPropertySource: String {
var arguments = ["name: \(stringLiteral(schemaName))"]
if let description {
arguments.append("description: \(description)")
}
arguments.append("type: \(type).self")
if !guides.isEmpty {
arguments.append("guides: [\(guides.map(\.description).joined(separator: ", "))]")
}
return "\(moduleName).GenerationSchema.Property(\(arguments.joined(separator: ", ")))"
}
/// The statement that initializes this property from `content`.
var initializationSource: String {
if let isOptional = generationIDOptionality, schemaName == "id" {
let fallback = isOptional ? "" : " ?? \(moduleName).GenerationID()"
return "self.\(identifier) = content.id\(fallback)"
}
return "self.\(identifier) = try content.value(forProperty: \(stringLiteral(schemaName)))"
}
}
extension GenerableProperty {
/// Collects the stored instance properties of a struct, in declaration order.
///
/// Static, `lazy`, and computed properties are skipped, as are `let` constants with an
/// initial value (they can't be assigned by `init(_:)`). Properties without a type annotation
/// are skipped with a warning.
///
/// - Parameters:
/// - declaration: The struct declaration.
/// - diagnose: Reports a diagnostic.
/// - Returns: The properties that become part of the schema.
static func storedProperties(
of declaration: some DeclGroupSyntax,
diagnose: (Diagnostic) -> Void
) -> [GenerableProperty] {
var properties: [GenerableProperty] = []
for member in declaration.memberBlock.members {
guard let variable = member.decl.as(VariableDeclSyntax.self) else { continue }
properties.append(contentsOf: storedProperties(of: variable, diagnose: diagnose))
}
return properties
}
/// Collects the stored properties declared by a single variable declaration.
///
/// - Parameters:
/// - variable: The variable declaration.
/// - diagnose: Reports a diagnostic.
/// - Returns: The properties that become part of the schema.
private static func storedProperties(
of variable: VariableDeclSyntax,
diagnose: (Diagnostic) -> Void
) -> [GenerableProperty] {
let modifiers = Set(variable.modifiers.map(\.name.text))
guard modifiers.isDisjoint(with: ["static", "class", "lazy"]) else { return [] }
let isLet = variable.bindingSpecifier.tokenKind == .keyword(.let)
let (description, guides) = guideArguments(of: variable.attributes)
// In `var a, b: Int`, `a` takes its type from the next binding that has one.
var bindings: [(name: TokenSyntax, type: TypeSyntax)] = []
var followingType: TypeSyntax?
for binding in variable.bindings.reversed() {
if let accessorBlock = binding.accessorBlock, accessorBlock.declaresComputedProperty {
followingType = binding.typeAnnotation?.type
continue
}
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
diagnose(
Diagnostic(node: binding.pattern, message: MacroDiagnostic.tuplePatternUnsupported))
followingType = nil
continue
}
if let annotation = binding.typeAnnotation {
followingType = annotation.type
} else if binding.initializer != nil {
followingType = nil
}
if isLet && binding.initializer != nil {
continue
}
guard let type = followingType else {
diagnose(
Diagnostic(
node: pattern,
message: MacroDiagnostic.missingTypeAnnotation(
propertyName: pattern.identifier.unescapedText)
)
)
continue
}
bindings.append((pattern.identifier, type))
}
return bindings.reversed().map {
GenerableProperty(
name: $0.name,
type: $0.type,
description: description,
guides: guides,
modifiers: variable.modifiers
)
}
}
/// Reads the description and guides from the `@Guide` attributes in `attributes`.
///
/// - Parameter attributes: The attributes of a variable declaration.
/// - Returns: The last `description:` argument and all unlabeled guide arguments.
private static func guideArguments(
of attributes: AttributeListSyntax
) -> (description: ExprSyntax?, guides: [ExprSyntax]) {
var description: ExprSyntax?
var guides: [ExprSyntax] = []
for element in attributes {
guard case .attribute(let attribute) = element, attribute.isGuideAttribute,
case .argumentList(let arguments) = attribute.arguments
else { continue }
for argument in arguments {
if argument.label?.text == "description" {
description = argument.expression.trimmed
} else {
guides.append(argument.expression.trimmed)
}
}
}
return (description, guides)
}
}
extension AttributeSyntax {
/// Whether this attribute is `@Guide` or `@FoundationModelsFake.Guide`.
var isGuideAttribute: Bool {
let name = attributeName.trimmedDescription
return name == "Guide" || name == "\(moduleName).Guide"
}
}
extension AccessorBlockSyntax {
/// Whether the accessors make the property computed (rather than stored with observers).
var declaresComputedProperty: Bool {
switch accessors {
case .getter:
return true
case .accessors(let accessors):
return accessors.contains {
$0.accessorSpecifier.tokenKind != .keyword(.willSet)
&& $0.accessorSpecifier.tokenKind != .keyword(.didSet)
}
}
}
}