Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,14 @@ public final class SwiftToSkeleton {
validatedJavaScriptModulePaths.insert(path)
}

let exportErrors = exportCollector.errors.filter { $0.severity == .error }
let importErrorsFatal = importCollector.errors.filter {
$0.severity == .error && !$0.message.contains("Unsupported type '")
}
let fileWarnings = (exportCollector.errors + importCollector.errors).filter { $0.severity == .warning }
let fileWarnings = importCollector.errors.filter { $0.severity == .warning }
warnings.append(contentsOf: fileWarnings.map { (file: inputFilePath, diagnostic: $0) })
if !exportErrors.isEmpty || !importErrorsFatal.isEmpty {
if !importErrorsFatal.isEmpty {
perSourceErrors.append(
(inputFilePath: inputFilePath, errors: exportErrors + importErrorsFatal)
(inputFilePath: inputFilePath, errors: importErrorsFatal)
)
}

Expand All @@ -249,6 +248,18 @@ public final class SwiftToSkeleton {
source.resolveDeferredExtensions(against: exportCollectors)
}

// We have to collect diagnostics after all deferred extensions are resolved, since they could generate some.
for ((_, inputFilePath), exportCollector) in zip(sourceFiles, exportCollectors) {
let exportErrors = exportCollector.errors.filter { $0.severity == .error }
let fileWarnings = exportCollector.errors.filter { $0.severity == .warning }
warnings.append(contentsOf: fileWarnings.map { (file: inputFilePath, diagnostic: $0) })
if !exportErrors.isEmpty {
perSourceErrors.append(
(inputFilePath: inputFilePath, errors: exportErrors)
)
}
}

for collector in exportCollectors {
collector.finalize(&exported)
}
Expand Down Expand Up @@ -858,6 +869,17 @@ extension AttributeListSyntax {
}
}

private final class JSAttributeFinder: SyntaxVisitor {
private(set) var found = false

override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
if node.attributeNameText == "JS" {
found = true
}
return .skipChildren
}
}

private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
var exportedFunctions: [ExportedFunction] = []
/// The names of the exported classes, in the order they were written in the source file
Expand Down Expand Up @@ -1910,7 +1932,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
break
}
}
if !resolved {
if !resolved, containsJSAnnotatedDeclaration(ext.memberBlock.members) {
diagnose(
node: ext.extendedType,
message: "Unsupported type '\(ext.extendedType.trimmedDescription)'.",
Expand All @@ -1920,6 +1942,12 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
}
}

private func containsJSAnnotatedDeclaration(_ members: MemberBlockItemListSyntax) -> Bool {
let finder = JSAttributeFinder(viewMode: .sourceAccurate)
finder.walk(members)
return finder.found
}

/// Walks extension members under the matching type’s state, returning whether the type was found.
///
/// Note: The lookup scans dictionaries keyed by `makeKey(name:namespace:)`, matching only by
Expand Down
38 changes: 38 additions & 0 deletions Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,44 @@ import Testing
}
}

@Test
func extensionOfUnknownTypeWithJSMemberProducesDiagnostic() throws {
let source = """
extension Unknown {
@JS func bridged() -> Int { 42 }
}
"""
let diagnostics = try #require(moduleDiagnostics(source: source))
#expect(diagnostics.description.contains("Unsupported type 'Unknown'"))
}

@Test
func extensionWithoutJSMembersIsIgnored() throws {
let source = """
extension String {
func helper() -> Int { 42 }
}
"""
#expect(moduleDiagnostics(source: source) == nil)
}

@Test
func invalidJSMemberInsideExtensionProducesDiagnostic() throws {
let source = """
@JS class Host {
@JS init() {}
}

extension Host {
@JS struct Bad {
var field = 1
}
}
"""
let diagnostics = try #require(moduleDiagnostics(source: source))
#expect(diagnostics.description.contains("Struct field must have explicit type annotation"))
}

@Test
func missingJavaScriptModuleProducesDiagnostic() throws {
let source = """
Expand Down
Loading