Usefulness of macros seems limited by inability to introspect types outside the current file/decl

For example, consider the following code:

// FeatureConfig.swift in module `Feature`
protocol FeatureConfig {
    var flag: Bool { get }
    var message: String { get }
}

// Feature.swift in module `Feature`, the type used across this module
public struct Feature {
    let config: FeatureConfig

    var flag: Bool { config.flag }
    var message: String { config.message }
}

// FeatureMockConfig.swift in module `MockConfig`
struct FeatureMockConfig: FeatureConfig {
    var flag: Bool
    var message: String
}

// Somewhere in the main app
let feature = Feature(config: FeatureMockConfig())

In my case at work, we have a whole lot of those "projections" of config.* properties into the feature module like this. It would be amazing if we could generate them automatically like this or something:

public struct Feature {
    let config: FeatureConfig

    #features(of: config)
}

Hell I would even settle for this:

public struct Feature {
    @config let config: FeatureConfig

    #config(flag)
    #config(message)
}

Is there no way SwiftSyntax could be modified to supply more information to macros for purposes like this?

1 Like

Is there a reason you can't use @dynamicMemberLookup for this?

As for the macro, attaching it to the overall type should allow inspection of its properties, which may allow you to generate what you want.

@dynamicMemberLookup is not ideal for this; we want autocomplete to work… I also don't think that would actually eliminate as much duplication as I'd like. The following code doesn't even compile for some reason anyway:

public struct Feature {
    let config: FeatureConfig

    subscript<T>(dynamicMember member: String) -> T {
        get {
            switch member {
            // Variable '<unknown>' captured by a closure before being initialized
            case "flag": config.flag as! T
            // Variable '<unknown>' captured by a closure before being initialized
            case "message": config.message as! T
            default: fatalError()
            }
        }
    }
}

As for the macro, attaching it to the overall type should allow inspection of its properties, which may allow you to generate what you want.

Attaching it to... which type? The config, protocol, or feature?

I want to generate code on the feature. Where can I get it from?

Using the KeyPath version of @dynamicMemberLookup autocompletes just fine. And you'd attach the macro to the Feature type, which should let you inspect its properties.

2 Likes

Ah! I forgot about that. Thanks for clarifying, sorry :)

Attaching the macro to Feature doesn't help me here, because I want to enumerate the properties of something else. Feature doesn't have any properties of its own besides config; I want to examine either FeatureMockConfig or FeatureConfig and duplicate the properties declared in either of those decls.

As @Jon_Shier said, @dynamicMemberLookup does the job, it also autocompletes!

// FeatureConfig.swift in module `Feature`
protocol FeatureConfig {
	var flag: Bool { get }
	var message: String { get }
}

// Feature.swift in module `Feature`, the type used across this module
@dynamicMemberLookup
public struct Feature {
	let config: any FeatureConfig

	subscript<T>(dynamicMember keyPath: KeyPath<any FeatureConfig, T>) -> T {
		config[keyPath: keyPath]
	}
}

// FeatureMockConfig.swift in module `MockConfig`
struct FeatureMockConfig: FeatureConfig {
	var flag = false
	var message = "test"
}

// Somewhere in the main app
let feature = Feature(config: FeatureMockConfig())
print(feature.flag) // false
print(feature.message) // "test"

Whoa! Sick! I see now! That's awesome. Thanks y'all!

I do still wish this sort outer-decl introspection was available with macros, though :woozy_face: As cool as this is, I suspect this won't enable satisfying requirements of a second protocol, would it? i.e.:

public protocol AFeatureDependencies {
    var flag: Bool { get }
    var message: String { get }
}

public strict AFeatureView: View {
    typealias Dependencies = AFeatureDependencies
    let dependencies: Dependencies
    ...
}

@dynamicMemberLookup
public struct Feature {
    let config: any FeatureConfig

    subscript<T>(dynamicMember keyPath: KeyPath<any FeatureConfig, T>) -> T {
        config[keyPath: keyPath]
    }
}

extension Feature: AFeatureView.Dependencies { }

If this works for that, then awesome! If not, problem persists

Edit: does not work, doesn't count as protocol conformance

No it can't provide protocol conformances, I believe this was pitched not too long ago but I can't look for it right now.