diff --git a/swift/internal/compiling.bzl b/swift/internal/compiling.bzl index f676c59e1..24326831a 100644 --- a/swift/internal/compiling.bzl +++ b/swift/internal/compiling.bzl @@ -761,9 +761,10 @@ def compile( # passed; the compiler does not attempt to load them when deserializing # modules. used_plugins = list(plugins) - for module_context in transitive_modules: - if module_context.swift and module_context.swift.plugins: - used_plugins.extend(module_context.swift.plugins) + for swift_info in swift_infos + private_swift_infos: + for module_context in swift_info.direct_modules: + if module_context.swift and module_context.swift.plugins: + used_plugins.extend(module_context.swift.plugins) if include_dev_srch_paths != None and is_test != None: fail("""\ diff --git a/test/BUILD b/test/BUILD index 7c6c97493..6c9ff0814 100644 --- a/test/BUILD +++ b/test/BUILD @@ -16,6 +16,7 @@ load(":imported_framework_tests.bzl", "imported_framework_test_suite") load(":interop_hints_tests.bzl", "interop_hints_test_suite") load(":layering_check_tests.bzl", "layering_check_test_suite") load(":localized_strings_tests.bzl", "localized_strings_test_suite") +load(":macro_plugin_tests.bzl", "macro_plugin_test_suite") load(":mainattr_tests.bzl", "mainattr_test_suite") load(":mixed_language_tests.bzl", "mixed_language_test_suite") load(":module_cache_settings_tests.bzl", "module_cache_settings_test_suite") @@ -70,6 +71,8 @@ layering_check_test_suite(name = "layering_check") localized_strings_test_suite(name = "localized_strings") +macro_plugin_test_suite(name = "macro_plugin") + mainattr_test_suite(name = "mainattr") mixed_language_test_suite(name = "mixed_language") diff --git a/test/fixtures/macros/BUILD b/test/fixtures/macros/BUILD new file mode 100644 index 000000000..8f1f72a19 --- /dev/null +++ b/test/fixtures/macros/BUILD @@ -0,0 +1,53 @@ +load("//swift:swift_compiler_plugin.bzl", "swift_compiler_plugin") +load("//swift:swift_library.bzl", "swift_library") +load("//test/fixtures:common.bzl", "FIXTURE_TAGS") + +package( + default_visibility = ["//test:__subpackages__"], +) + +licenses(["notice"]) + +############################################################################### +# Fixtures for testing compiler plugin (macro) propagation + +swift_compiler_plugin( + name = "stringify_macro", + srcs = [ + "StringifyMacro.swift", + "StringifyMacroPlugin.swift", + ], + module_name = "StringifyMacroPlugin", + tags = FIXTURE_TAGS, + deps = [ + "@swift-syntax//:SwiftCompilerPlugin", + "@swift-syntax//:SwiftSyntax", + "@swift-syntax//:SwiftSyntaxBuilder", + "@swift-syntax//:SwiftSyntaxMacroExpansion", + "@swift-syntax//:SwiftSyntaxMacros", + ], +) + +swift_library( + name = "stringify", + srcs = ["Stringify.swift"], + module_name = "Stringify", + plugins = [":stringify_macro"], + tags = FIXTURE_TAGS, +) + +swift_library( + name = "stringify_user", + srcs = ["StringifyUser.swift"], + module_name = "StringifyUser", + tags = FIXTURE_TAGS, + deps = [":stringify"], +) + +swift_library( + name = "transitive_user", + srcs = ["TransitiveUser.swift"], + module_name = "TransitiveUser", + tags = FIXTURE_TAGS, + deps = [":stringify_user"], +) diff --git a/test/fixtures/macros/Stringify.swift b/test/fixtures/macros/Stringify.swift new file mode 100644 index 000000000..304af8120 --- /dev/null +++ b/test/fixtures/macros/Stringify.swift @@ -0,0 +1,17 @@ +// Copyright 2023 The Bazel Authors. All rights reserved. +// +// 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. + +@freestanding(expression) +public macro stringify(_ value: T) -> (T, String) = + #externalMacro(module: "StringifyMacroPlugin", type: "StringifyMacro") diff --git a/test/fixtures/macros/StringifyMacro.swift b/test/fixtures/macros/StringifyMacro.swift new file mode 100644 index 000000000..091a6c1f2 --- /dev/null +++ b/test/fixtures/macros/StringifyMacro.swift @@ -0,0 +1,29 @@ +// Copyright 2023 The Bazel Authors. All rights reserved. +// +// 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 SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct StringifyMacro: ExpressionMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) -> ExprSyntax { + guard let argument = node.argumentList.first?.expression else { + fatalError("compiler bug: the macro does not have any arguments") + } + return "(\(argument), \(literal: argument.description))" + } +} diff --git a/test/fixtures/macros/StringifyMacroPlugin.swift b/test/fixtures/macros/StringifyMacroPlugin.swift new file mode 100644 index 000000000..e363c309e --- /dev/null +++ b/test/fixtures/macros/StringifyMacroPlugin.swift @@ -0,0 +1,25 @@ +// Copyright 2023 The Bazel Authors. All rights reserved. +// +// 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. + +#if canImport(SwiftCompilerPlugin) +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct StringifyMacroPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + StringifyMacro.self + ] +} +#endif diff --git a/test/fixtures/macros/StringifyUser.swift b/test/fixtures/macros/StringifyUser.swift new file mode 100644 index 000000000..27f8115be --- /dev/null +++ b/test/fixtures/macros/StringifyUser.swift @@ -0,0 +1,21 @@ +// Copyright 2026 The Bazel Authors. All rights reserved. +// +// 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 Stringify + +// Directly depends on the macro-declaring module and contains an expansion +// site, so its compilation must load the plugin. +public func stringifyUser() -> (Int, String) { + #stringify(1 + 1) +} diff --git a/test/fixtures/macros/TransitiveUser.swift b/test/fixtures/macros/TransitiveUser.swift new file mode 100644 index 000000000..cac8242a0 --- /dev/null +++ b/test/fixtures/macros/TransitiveUser.swift @@ -0,0 +1,22 @@ +// Copyright 2026 The Bazel Authors. All rights reserved. +// +// 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 StringifyUser + +// Only sees the macro-declaring module transitively (through StringifyUser) +// and contains no expansion sites, so its compilation must not load the +// plugin. +public func transitiveUser() -> String { + String(describing: stringifyUser()) +} diff --git a/test/macro_plugin_tests.bzl b/test/macro_plugin_tests.bzl new file mode 100644 index 000000000..86f4837ad --- /dev/null +++ b/test/macro_plugin_tests.bzl @@ -0,0 +1,66 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# 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. + +"""Tests for compiler plugin (macro) propagation.""" + +load( + "//test/rules:action_command_line_test.bzl", + "action_command_line_test", +) + +def macro_plugin_test_suite(name, tags = []): + """Test suite for compiler plugin propagation to dependents. + + Args: + name: The base name to be used in targets created by this macro. + tags: Additional tags to apply to each test. + """ + all_tags = [name] + tags + + # The module that declares the macro loads its own plugin. + action_command_line_test( + name = "{}_declaring_module_loads_plugin".format(name), + expected_argv = ["-load-plugin-executable"], + mnemonic = "SwiftCompile", + tags = all_tags, + target_under_test = "//test/fixtures/macros:stringify", + ) + + # A module that directly depends on the library declaring the macro must + # load the plugin, since its sources may contain expansion sites. + action_command_line_test( + name = "{}_direct_dependent_loads_plugin".format(name), + expected_argv = ["-load-plugin-executable"], + mnemonic = "SwiftCompile", + tags = all_tags, + target_under_test = "//test/fixtures/macros:stringify_user", + ) + + # A module that only sees the macro-declaring module transitively must + # not: macros are source-level transformations, so the compiler never + # loads plugins when deserializing dependency modules. Passing the plugin + # anyway makes its executable an input to every transitive consumer's + # compilation, so any change to the plugin recompiles all of them. + action_command_line_test( + name = "{}_transitive_dependent_does_not_load_plugin".format(name), + mnemonic = "SwiftCompile", + not_expected_argv = ["-load-plugin-executable"], + tags = all_tags, + target_under_test = "//test/fixtures/macros:transitive_user", + ) + + native.test_suite( + name = name, + tags = all_tags, + )