Skip to content

Commit 22c0516

Browse files
committed
improved diagnostics
1 parent a2f98dd commit 22c0516

2 files changed

Lines changed: 70 additions & 28 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,11 @@ public final class SwiftToSkeleton {
103103
if importedJSModules[path] != nil {
104104
continue
105105
}
106+
let pathNode = importCollector.importedModulePathNodes[path] ?? Syntax(sourceFile)
106107
guard path.hasPrefix("/") else {
107108
importCollector.errors.append(
108109
DiagnosticError(
109-
node: sourceFile,
110+
node: pathNode,
110111
message: "JavaScript module paths must start with '/' to indicate the Swift target root: "
111112
+ "'\(path)'."
112113
)
@@ -116,7 +117,7 @@ public final class SwiftToSkeleton {
116117
guard let source = try javaScriptModuleSource(path) else {
117118
importCollector.errors.append(
118119
DiagnosticError(
119-
node: sourceFile,
120+
node: pathNode,
120121
message: "JavaScript module file was not found at '\(path)'."
121122
)
122123
)
@@ -2452,6 +2453,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
24522453
var importedFunctions: [ImportedFunctionSkeleton] = []
24532454
var importedTypes: [ImportedTypeSkeleton] = []
24542455
var importedGlobalGetters: [ImportedGetterSkeleton] = []
2456+
var importedModulePathNodes: [String: Syntax] = [:]
24552457
var errors: [DiagnosticError] = []
24562458

24572459
private let inputFilePath: String
@@ -2546,31 +2548,41 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25462548
}
25472549
return nil
25482550
}
2551+
}
25492552

2550-
/// Extracts the `from` argument value from an attribute, if present.
2551-
static func extractJSImportFrom(from attribute: AttributeSyntax) -> JSImportFrom? {
2552-
guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) else {
2553+
private func extractJSImportFrom(from attribute: AttributeSyntax) -> JSImportFrom? {
2554+
guard let arguments = attribute.arguments?.as(LabeledExprListSyntax.self),
2555+
let argument = arguments.first(where: { $0.label?.text == "from" })
2556+
else {
2557+
return nil
2558+
}
2559+
2560+
if let call = argument.expression.as(FunctionCallExprSyntax.self),
2561+
call.calledExpression.trimmedDescription.split(separator: ".").last == "module"
2562+
{
2563+
guard call.arguments.count == 1,
2564+
let pathExpression = call.arguments.first?.expression,
2565+
let literal = pathExpression.as(StringLiteralExprSyntax.self),
2566+
let path = literal.representedLiteralValue
2567+
else {
2568+
errors.append(
2569+
DiagnosticError(
2570+
node: call.arguments.first?.expression ?? argument.expression,
2571+
message: "JavaScript module path must be a string literal."
2572+
)
2573+
)
25532574
return nil
25542575
}
2555-
for argument in arguments {
2556-
guard argument.label?.text == "from" else { continue }
2557-
2558-
if let call = argument.expression.as(FunctionCallExprSyntax.self),
2559-
call.calledExpression.trimmedDescription.split(separator: ".").last == "module",
2560-
call.arguments.count == 1,
2561-
let literal = call.arguments.first?.expression.as(StringLiteralExprSyntax.self),
2562-
let path = literal.representedLiteralValue
2563-
{
2564-
return .module(path)
2565-
}
2566-
2567-
// Accept `.global`, `JSImportFrom.global`, etc.
2568-
let description = argument.expression.trimmedDescription
2569-
let caseName = description.split(separator: ".").last.map(String.init) ?? description
2570-
return caseName == "global" ? .global : nil
2576+
if importedModulePathNodes[path] == nil {
2577+
importedModulePathNodes[path] = Syntax(literal)
25712578
}
2572-
return nil
2579+
return .module(path)
25732580
}
2581+
2582+
// Accept `.global`, `JSImportFrom.global`, etc.
2583+
let description = argument.expression.trimmedDescription
2584+
let caseName = description.split(separator: ".").last.map(String.init) ?? description
2585+
return caseName == "global" ? .global : nil
25742586
}
25752587

25762588
// MARK: - Validation Helpers
@@ -2753,7 +2765,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
27532765
if AttributeChecker.hasJSClassAttribute(node.attributes) {
27542766
let attribute = AttributeChecker.firstJSClassAttribute(node.attributes)
27552767
let jsName = attribute.flatMap(AttributeChecker.extractJSName)
2756-
let from = attribute.flatMap(AttributeChecker.extractJSImportFrom)
2768+
let from = attribute.flatMap { extractJSImportFrom(from: $0) }
27572769
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
27582770
enterJSClass(node.name.text, jsName: jsName, from: from, accessLevel: accessLevel)
27592771
}
@@ -2770,7 +2782,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
27702782
if AttributeChecker.hasJSClassAttribute(node.attributes) {
27712783
let attribute = AttributeChecker.firstJSClassAttribute(node.attributes)
27722784
let jsName = attribute.flatMap(AttributeChecker.extractJSName)
2773-
let from = attribute.flatMap(AttributeChecker.extractJSImportFrom)
2785+
let from = attribute.flatMap { extractJSImportFrom(from: $0) }
27742786
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
27752787
enterJSClass(node.name.text, jsName: jsName, from: from, accessLevel: accessLevel)
27762788
}
@@ -2964,7 +2976,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
29642976

29652977
let baseName = SwiftToSkeleton.normalizeIdentifier(node.name.text)
29662978
let jsName = AttributeChecker.extractJSName(from: jsFunction)
2967-
let from = AttributeChecker.extractJSImportFrom(from: jsFunction)
2979+
let from = extractJSImportFrom(from: jsFunction)
29682980
let name = baseName
29692981

29702982
let parameters = parseParameters(from: node.signature.parameterClause)
@@ -3017,7 +3029,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
30173029
}
30183030
let propertyName = SwiftToSkeleton.normalizeIdentifier(identifier.identifier.text)
30193031
let jsName = AttributeChecker.extractJSName(from: jsGetter)
3020-
let from = AttributeChecker.extractJSImportFrom(from: jsGetter)
3032+
let from = extractJSImportFrom(from: jsGetter)
30213033
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
30223034
return ImportedGetterSkeleton(
30233035
name: propertyName,

Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,46 @@ import Testing
2828

2929
@Test
3030
func missingJavaScriptModuleProducesDiagnostic() throws {
31-
let source = "@JSFunction(from: .module(\"/missing.js\")) func imported() throws(JSException)"
31+
let source = """
32+
let unrelated = 0
33+
@JSFunction(from: .module("/missing.js")) func imported() throws(JSException)
34+
"""
3235
let diagnostics = try #require(moduleDiagnostics(source: source))
3336
#expect(diagnostics.description.contains("JavaScript module file was not found at '/missing.js'"))
37+
#expect(diagnostics.description.contains("test.swift:2:27:"))
3438
}
3539

3640
@Test
3741
func javaScriptModulePathMustStartAtTargetRoot() throws {
38-
let source = "@JSFunction(from: .module(\"missing.js\")) func imported() throws(JSException)"
42+
let source = """
43+
let unrelated = 0
44+
@JSFunction(from: .module("missing.js")) func imported() throws(JSException)
45+
"""
3946
let diagnostics = try #require(moduleDiagnostics(source: source))
4047
#expect(diagnostics.description.contains("JavaScript module paths must start with '/'"))
48+
#expect(diagnostics.description.contains("test.swift:2:27:"))
49+
}
50+
51+
@Test
52+
func missingJavaScriptModuleWithTraversalProducesDiagnosticAtPath() throws {
53+
let source = """
54+
let unrelated = 0
55+
@JSFunction(from: .module("/../missing.js")) func imported() throws(JSException)
56+
"""
57+
let diagnostics = try #require(moduleDiagnostics(source: source))
58+
#expect(diagnostics.description.contains("JavaScript module file was not found at '/../missing.js'"))
59+
#expect(diagnostics.description.contains("test.swift:2:27:"))
60+
}
61+
62+
@Test
63+
func javaScriptModulePathMustBeStringLiteral() throws {
64+
let source = """
65+
let modulePath = "/module.js"
66+
@JSFunction(from: .module(modulePath)) func imported() throws(JSException)
67+
"""
68+
let diagnostics = try #require(moduleDiagnostics(source: source))
69+
#expect(diagnostics.description.contains("JavaScript module path must be a string literal."))
70+
#expect(diagnostics.description.contains("test.swift:2:27:"))
4171
}
4272

4373
/// Returns the first parameter's type node from a function in the source (the first `@JS func`-like decl), for pinpointing diagnostics.

0 commit comments

Comments
 (0)