Skip to content

Commit bbdf7d5

Browse files
authored
Merge pull request #660 from swiftwasm/katei/b8fc-ts2swift-should
TS2Swift: warn on skipped exports
2 parents 6bb09b6 + eace50e commit bbdf7d5

File tree

5 files changed

+97
-9
lines changed

5 files changed

+97
-9
lines changed

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/cli.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,13 @@ Examples:
9797
/**
9898
* Run ts2swift for a single input file (programmatic API, no process I/O).
9999
* @param {string[]} filePaths - Paths to the .d.ts files
100-
* @param {{ tsconfigPath: string, logLevel?: keyof typeof DiagnosticEngine.LEVELS, globalFiles?: string[] }} options
100+
* @param {{ tsconfigPath: string, logLevel?: keyof typeof DiagnosticEngine.LEVELS, globalFiles?: string[], diagnosticEngine?: DiagnosticEngine }} options
101101
* @returns {string} Generated Swift source
102102
* @throws {Error} on parse/type-check errors (diagnostics are included in the message)
103103
*/
104104
export function run(filePaths, options) {
105-
const { tsconfigPath, logLevel = 'info', globalFiles = [] } = options;
106-
const diagnosticEngine = new DiagnosticEngine(logLevel);
105+
const { tsconfigPath, logLevel = 'info', globalFiles = [], diagnosticEngine } = options;
106+
const engine = diagnosticEngine ?? new DiagnosticEngine(logLevel);
107107

108108
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
109109
const configParseResult = ts.parseJsonConfigFileContent(
@@ -164,7 +164,7 @@ export function run(filePaths, options) {
164164
const bodies = [];
165165
const globalFileSet = new Set(globalFiles);
166166
for (const inputPath of [...filePaths, ...globalFiles]) {
167-
const processor = new TypeProcessor(program.getTypeChecker(), diagnosticEngine, {
167+
const processor = new TypeProcessor(program.getTypeChecker(), engine, {
168168
defaultImportFromGlobal: globalFileSet.has(inputPath),
169169
});
170170
const result = processor.processTypeDeclarations(program, inputPath);
@@ -247,7 +247,7 @@ export function main(args) {
247247

248248
let swiftOutput;
249249
try {
250-
swiftOutput = run(filePaths, { tsconfigPath, logLevel, globalFiles });
250+
swiftOutput = run(filePaths, { tsconfigPath, logLevel, globalFiles, diagnosticEngine });
251251
} catch (/** @type {unknown} */ err) {
252252
if (err instanceof Error) {
253253
diagnosticEngine.print("error", err.message);

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export class TypeProcessor {
5757
/** @type {Set<string>} */
5858
this.emittedStringLiteralUnionNames = new Set();
5959

60+
/** @type {Set<ts.Node>} */
61+
this.warnedExportNodes = new Set();
62+
6063
/** @type {Set<string>} */
6164
this.visitedDeclarationKeys = new Set();
6265

@@ -192,6 +195,8 @@ export class TypeProcessor {
192195
this.visitEnumDeclaration(node);
193196
} else if (ts.isExportDeclaration(node)) {
194197
this.visitExportDeclaration(node);
198+
} else if (ts.isExportAssignment(node)) {
199+
this.visitExportAssignment(node);
195200
}
196201
}
197202

@@ -239,6 +244,7 @@ export class TypeProcessor {
239244
}
240245
} else {
241246
// export * as ns from "..." is not currently supported by BridgeJS imports.
247+
this.warnExportSkip(node, "Skipping namespace re-export (export * as ns) which is not supported");
242248
return;
243249
}
244250

@@ -254,6 +260,19 @@ export class TypeProcessor {
254260
this.visitNode(declaration);
255261
}
256262
}
263+
264+
if (targetSymbols.length === 0) {
265+
this.warnExportSkip(node, "Export declaration resolved to no symbols; nothing was generated");
266+
}
267+
}
268+
269+
/**
270+
* Handle `export default foo;` style assignments.
271+
* @param {ts.ExportAssignment} node
272+
*/
273+
visitExportAssignment(node) {
274+
// BridgeJS does not currently model default export assignments (they may point to expressions).
275+
this.warnExportSkip(node, "Skipping export assignment (export default ...) which is not supported");
257276
}
258277

259278
/**
@@ -271,7 +290,10 @@ export class TypeProcessor {
271290
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
272291

273292
for (const decl of node.declarationList.declarations) {
274-
if (!ts.isIdentifier(decl.name)) continue;
293+
if (!ts.isIdentifier(decl.name)) {
294+
this.warnExportSkip(decl, "Skipping exported variable with a non-identifier name");
295+
continue;
296+
}
275297

276298
const jsName = decl.name.text;
277299
const swiftName = this.swiftTypeName(jsName);
@@ -399,7 +421,12 @@ export class TypeProcessor {
399421
*/
400422
visitEnumDeclaration(node) {
401423
const name = node.name?.text;
402-
if (!name) return;
424+
if (!name) {
425+
if (this.isExported(node)) {
426+
this.warnExportSkip(node, "Skipping exported enum without a name");
427+
}
428+
return;
429+
}
403430
this.emitEnumFromDeclaration(name, node, node);
404431
}
405432

@@ -532,7 +559,12 @@ export class TypeProcessor {
532559
* @private
533560
*/
534561
visitFunctionDeclaration(node) {
535-
if (!node.name) return;
562+
if (!node.name) {
563+
if (this.isExported(node)) {
564+
this.warnExportSkip(node, "Skipping exported function without a name");
565+
}
566+
return;
567+
}
536568
const jsName = node.name.text;
537569
const swiftName = this.swiftTypeName(jsName);
538570
const fromArg = this.renderDefaultJSImportFromArgument();
@@ -774,7 +806,12 @@ export class TypeProcessor {
774806
* @private
775807
*/
776808
visitClassDecl(node) {
777-
if (!node.name) return;
809+
if (!node.name) {
810+
if (this.isExported(node)) {
811+
this.warnExportSkip(node, "Skipping exported class without a name");
812+
}
813+
return;
814+
}
778815

779816
const jsName = node.name.text;
780817
if (this.emittedStructuredTypeNames.has(jsName)) return;
@@ -1244,6 +1281,28 @@ export class TypeProcessor {
12441281
return parts.join(" ");
12451282
}
12461283

1284+
/**
1285+
* @param {ts.Node} node
1286+
* @returns {boolean}
1287+
*/
1288+
isExported(node) {
1289+
const hasExportModifier = /** @type {ts.ModifierLike[] | undefined} */ (node.modifiers)?.some(
1290+
(m) => m.kind === ts.SyntaxKind.ExportKeyword
1291+
) ?? false;
1292+
return hasExportModifier || ts.isExportAssignment(node);
1293+
}
1294+
1295+
/**
1296+
* Emit a single warning per node when an exported declaration cannot be generated.
1297+
* @param {ts.Node} node
1298+
* @param {string} reason
1299+
*/
1300+
warnExportSkip(node, reason) {
1301+
if (this.warnedExportNodes.has(node)) return;
1302+
this.warnedExportNodes.add(node);
1303+
this.diagnosticEngine.print("warning", `${reason}. Swift binding not generated`, node);
1304+
}
1305+
12471306
/**
12481307
* Render identifier with backticks if needed
12491308
* @param {string} name

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ exports[`ts2swift > snapshots Swift output for Documentation.d.ts > Documentatio
121121
"
122122
`;
123123

124+
exports[`ts2swift > snapshots Swift output for ExportAssignment.d.ts > ExportAssignment 1`] = `
125+
"// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
126+
// DO NOT EDIT.
127+
//
128+
// To update this file, just rebuild your project or run
129+
// \`swift package bridge-js\`.
130+
131+
@_spi(BridgeJS) import JavaScriptKit
132+
133+
@JSGetter var foo: Double
134+
"
135+
`;
136+
124137
exports[`ts2swift > snapshots Swift output for Interface.d.ts > Interface 1`] = `
125138
"// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
126139
// DO NOT EDIT.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const foo: number;
2+
export default foo;

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/ts2swift.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,18 @@ describe('ts2swift', () => {
5050
rmSync(tmpDir, { recursive: true, force: true });
5151
}
5252
});
53+
54+
it('emits a warning when export assignments cannot be generated', () => {
55+
const dtsPath = path.join(inputsDir, 'ExportAssignment.d.ts');
56+
/** @type {{ level: string, message: string }[]} */
57+
const diagnostics = [];
58+
const diagnosticEngine = {
59+
print: (level, message) => diagnostics.push({ level, message }),
60+
};
61+
run([dtsPath], { tsconfigPath, logLevel: 'warning', diagnosticEngine });
62+
const messages = diagnostics.map((d) => d.message).join('\n');
63+
expect(messages).toMatch(/Skipping export assignment/);
64+
const occurrences = (messages.match(/Skipping export assignment/g) || []).length;
65+
expect(occurrences).toBe(1);
66+
});
5367
});

0 commit comments

Comments
 (0)