Skip to content

Commit 75290b5

Browse files
authored
Warn when a native View is marked as @NoBridge (#252)
1 parent dd16561 commit 75290b5

5 files changed

Lines changed: 68 additions & 11 deletions

File tree

Sources/SkipBuild/SkipProject.swift

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,10 +1048,11 @@ public class \(moduleName)Module {
10481048

10491049
// only create tests if we have specified to do so, and we are not a dependent native module
10501050
let createTestModule = createModuleTests && !isDependentNativeModule && !isNativeAppModule
1051+
let testsModuleName = moduleName + "Tests"
10511052

10521053
if createTestModule {
10531054
let testsURL = try projectFolderURL.append(path: "Tests", create: true)
1054-
let testDir = try testsURL.append(path: moduleName + "Tests", create: true)
1055+
let testDir = try testsURL.append(path: testsModuleName, create: true)
10551056
let testSkipDir = try testDir.append(path: "Skip", create: true)
10561057
let testSwiftFile = testDir.appending(path: "\(moduleName)Tests.swift")
10571058

@@ -1301,20 +1302,30 @@ struct TestData : Codable, Hashable {
13011302

13021303
try testCaseCode.write(to: testSwiftFile, atomically: false, encoding: .utf8)
13031304

1304-
let skipYamlAppTests = """
1305-
# # Skip configuration for \(moduleName) module
1306-
#build:
1307-
# contents:
1308-
"""
1305+
var testSkipYaml = """
1306+
# Skip configuration for \(testsModuleName) module
13091307
1310-
let skipYamlModuleTests = """
1311-
# # Skip configuration for \(moduleName) module
13121308
#build:
13131309
# contents:
1310+
13141311
"""
13151312

1313+
if moduleMode.isNative {
1314+
// The test target for a natively-compiled Skip Fuse module must itself be transpiled: its
1315+
// XCTest cases are transpiled to JUnit tests so the test harness can collect the results.
1316+
// A native test target would have its test classes dropped during bridging, leaving no
1317+
// tests to run, so it must explicitly opt into transpiled mode.
1318+
testSkipYaml += """
1319+
1320+
skip:
1321+
mode: 'transpiled'
1322+
1323+
"""
1324+
1325+
}
1326+
13161327
let testSkipYamlFile = testSkipDir.appending(path: "skip.yml")
1317-
try (isAppModule ? skipYamlAppTests : skipYamlModuleTests).write(to: testSkipYamlFile, atomically: false, encoding: .utf8)
1328+
try testSkipYaml.write(to: testSkipYamlFile, atomically: false, encoding: .utf8)
13181329

13191330
if let resourceFolder = resourceFolder, !resourceFolder.isEmpty {
13201331
let testResourcesDir = try testDir.append(path: resourceFolder, create: true)
@@ -1438,7 +1449,7 @@ struct TestData : Codable, Hashable {
14381449
let skipTestDependency = ",\n \(skipTestProduct)\n ]"
14391450

14401451
targets += """
1441-
.testTarget(name: "\(moduleName)Tests", dependencies: [
1452+
.testTarget(name: "\(testsModuleName)", dependencies: [
14421453
"\(moduleName)"\(skipTestDependency)\(resourcesAttribute), plugins: \(skipPluginArray)),
14431454
14441455
"""

Sources/SkipSyntax/Message.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ extension Message {
147147
return Message(kind: .warning, message: "Skip does not understand complex preprocessor directives. When using Skip-related preprocessor symbols, use only SYMBOL, !SYMBOL, or a list where all symbols are combined by either && or || (but not a combination of the two)", source: source, sourceRange: range)
148148
}
149149

150+
static func noBridgeView(_ syntax: SyntaxProtocol, source: Source) -> Message {
151+
let range = syntax.range(in: source)
152+
return Message(kind: .warning, message: "A SwiftUI View must be bridged in order to render as a native view, but `// SKIP @nobridge` prevents bridging, so this view will not appear on Android. Remove the `// SKIP @nobridge` directive to allow this view to render.", source: source, sourceRange: range)
153+
}
154+
150155
// Idea: translate subscripts to Kotlin get/set operator functions
151156
static func subscriptNotSupported(_ syntax: SyntaxProtocol, source: Source) -> Message {
152157
let range = syntax.range(in: source)

Sources/SkipSyntax/StatementTypes.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,11 @@ class TypeDeclaration: Statement {
15741574
// We don't care about other protocols
15751575
inherits = [inheritsView]
15761576
} else {
1577+
// A native SwiftUI view renders only via its generated bridge, so `// SKIP @nobridge` keeps it
1578+
// from ever appearing. Warn instead of silently producing a view that never renders.
1579+
if attributes.isNoBridge, syntaxTree.isBridgeFile, syntaxTree.autoBridge != .none, inherits.contains(where: { isSwiftUIType($0) }) {
1580+
syntaxTree.root.messages.append(.noBridgeView(structDecl, source: syntaxTree.source))
1581+
}
15771582
return nil
15781583
}
15791584
}
@@ -1628,6 +1633,11 @@ class TypeDeclaration: Statement {
16281633
// We don't care about other protocols
16291634
inherits = [inheritsView]
16301635
} else {
1636+
// A native SwiftUI view renders only via its generated bridge, so `// SKIP @nobridge` keeps it
1637+
// from ever appearing. Warn instead of silently producing a view that never renders.
1638+
if attributes.isNoBridge, syntaxTree.isBridgeFile, syntaxTree.autoBridge != .none, inherits.contains(where: { isSwiftUIType($0) }) {
1639+
syntaxTree.root.messages.append(.noBridgeView(enumDecl, source: syntaxTree.source))
1640+
}
16311641
return nil
16321642
}
16331643
}

Tests/SkipBuildTests/SkipCommandTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,20 @@ final class SkipCommandTests: XCTestCase {
784784
785785
""")
786786

787+
// the test target for a native module must explicitly opt into transpiled mode so its
788+
// XCTest cases become JUnit tests (a native test target would have its test classes dropped)
789+
let TestsSkipYML = try load("Tests/SomeModuleTests/Skip/skip.yml")
790+
XCTAssertEqual(TestsSkipYML, """
791+
# Skip configuration for SomeModuleTests module
792+
793+
#build:
794+
# contents:
795+
796+
skip:
797+
mode: 'transpiled'
798+
799+
""")
800+
787801
let PackageSwift = try load("Package.swift")
788802
XCTAssertEqual(PackageSwift, """
789803
// swift-tools-version: 6.1

Tests/SkipSyntaxTests/BridgeToKotlinTests.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7513,7 +7513,10 @@ final class BridgeToKotlinTests: XCTestCase {
75137513
}
75147514

75157515
func testNoBridgeView() async throws {
7516-
try await check(swiftBridge: """
7516+
// A `// SKIP @nobridge` SwiftUI View renders only via its bridge, so suppressing the bridge would leave
7517+
// it blank. The view is therefore not bridged (no output), and the transpiler emits a warning
7518+
// explaining that the view must be bridged in order to render as a native view.
7519+
try await check(expectMessages: true, swiftBridge: """
75177520
import SkipFuseUI
75187521
// SKIP @nobridge
75197522
struct V: View {
@@ -7527,6 +7530,20 @@ final class BridgeToKotlinTests: XCTestCase {
75277530
""", transformers: transformers)
75287531
}
75297532

7533+
func testNoBridgeNonView() async throws {
7534+
// @nobridge still fully suppresses bridging for non-view types, and (unlike a @nobridge View) must
7535+
// NOT produce the "must be bridged in order to render" warning, which is scoped to SwiftUI views.
7536+
try await check(swiftBridge: """
7537+
import SkipFuseUI
7538+
// SKIP @nobridge
7539+
public struct Helper {
7540+
public var count = 1
7541+
}
7542+
""", kotlin: """
7543+
""", swiftBridgeSupport: """
7544+
""", transformers: transformers)
7545+
}
7546+
75307547
func testNoBridgeInFuseModeBlock() async throws {
75317548
try await check(swiftBridge: """
75327549
import SkipFuseUI

0 commit comments

Comments
 (0)