Skip to content

Commit 5d515cb

Browse files
authored
Prevented skipping nodes during mergeVariablesIntoClasses (dart-archive#56)
* Prevented skipping nodes during mergeVariablesIntoClasses Nodes were being removed from the source file as the source file was being traversed, which was causing problems in some cases. Nodes are now replaced by EmptyStatements rather than being removed. * Handled AST removals/replacements using a map instead of directly modifying the AST
1 parent 47014ea commit 5d515cb

File tree

4 files changed

+53
-56
lines changed

4 files changed

+53
-56
lines changed

lib/base.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -282,15 +282,6 @@ export function copyNodeArrayLocation(src: ts.TextRange, dest: ts.NodeArray<any>
282282
dest.end = src.end;
283283
}
284284

285-
// Polyfill for ES6 Array.find.
286-
export function arrayFindPolyfill<T extends ts.Node>(
287-
nodeArray: ts.NodeArray<T>, predicate: (node: T) => boolean): T {
288-
for (let i = 0; i < nodeArray.length; ++i) {
289-
if (predicate(nodeArray[i])) return nodeArray[i];
290-
}
291-
return null;
292-
}
293-
294285
export function getAncestor(n: ts.Node, kind: ts.SyntaxKind): ts.Node {
295286
for (let parent = n; parent; parent = parent.parent) {
296287
if (parent.kind === kind) return parent;
@@ -454,6 +445,22 @@ export class TranspilerBase {
454445
this.transpiler.reportError(n, message);
455446
}
456447

448+
/**
449+
* Prevents this node from being visited.
450+
*/
451+
suppressNode(n: ts.Node) {
452+
const emptyNode = ts.createNode(ts.SyntaxKind.EmptyStatement);
453+
copyLocation(n, emptyNode);
454+
this.replaceNode(n, emptyNode);
455+
}
456+
457+
/**
458+
* Effectively replaces original with replacement in the AST.
459+
*/
460+
replaceNode(original: ts.Node, replacement: ts.Node) {
461+
this.transpiler.nodeSubstitutions.set(original, replacement);
462+
}
463+
457464
visitNode(n: ts.Node): boolean {
458465
throw new Error('not implemented');
459466
}

lib/main.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ export class Transpiler {
8989
* Map of import library path to a Set of identifier names being imported.
9090
*/
9191
imports: Map<String, ImportSummary>;
92+
/**
93+
* Map containing AST nodes that we have removed or replaced. This is safer than modifying the AST
94+
* directly.
95+
*/
96+
nodeSubstitutions: Map<ts.Node, ts.Node> = new Map();
9297
// Comments attach to all following AST nodes before the next 'physical' token. Track the earliest
9398
// offset to avoid printing comments multiple times.
9499
private lastCommentIdx = -1;
@@ -421,6 +426,9 @@ export class Transpiler {
421426
}
422427

423428
visit(node: ts.Node) {
429+
if (this.nodeSubstitutions.has(node)) {
430+
node = this.nodeSubstitutions.get(node);
431+
}
424432
if (!node) return;
425433
let comments = ts.getLeadingCommentRanges(this.currentFile.text, node.getFullStart());
426434
if (comments) {

lib/merge.ts

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ export function normalizeSourceFile(f: ts.SourceFile, fc: FacadeConverter, expli
331331
clazz.name = declaration.name as ts.Identifier;
332332
clazz.members = ts.createNodeArray();
333333
base.copyNodeArrayLocation(declaration, clazz.members);
334-
replaceNode(n, clazz);
334+
fc.replaceNode(n, clazz);
335335
classes.set(name, clazz);
336336
}
337337

@@ -347,7 +347,7 @@ export function normalizeSourceFile(f: ts.SourceFile, fc: FacadeConverter, expli
347347
let type: ts.TypeNode = declaration.type;
348348
if (ts.isTypeLiteralNode(type)) {
349349
if (existingClass) {
350-
removeNode(n);
350+
fc.suppressNode(n);
351351
}
352352
type.members.forEach((member: ts.TypeElement) => {
353353
switch (member.kind) {
@@ -454,32 +454,6 @@ export function normalizeSourceFile(f: ts.SourceFile, fc: FacadeConverter, expli
454454
return propertyDeclarations;
455455
}
456456

457-
function removeFromArray(nodes: ts.NodeArray<ts.Node>, v: ts.Node) {
458-
for (let i = 0, len = nodes.length; i < len; ++i) {
459-
if (nodes[i] === v) {
460-
// Small hack to get around NodeArrays being readonly
461-
Array.prototype.splice.call(nodes, i, 1);
462-
break;
463-
}
464-
}
465-
}
466-
467-
function removeNode(n: ts.Node) {
468-
let parent = n.parent;
469-
switch (parent.kind) {
470-
case ts.SyntaxKind.ModuleBlock:
471-
let block = <ts.ModuleBlock>parent;
472-
removeFromArray(block.statements, n);
473-
break;
474-
case ts.SyntaxKind.SourceFile:
475-
let sourceFile = <ts.SourceFile>parent;
476-
removeFromArray(sourceFile.statements, n);
477-
break;
478-
default:
479-
throw 'removeNode not implemented for kind:' + parent.kind;
480-
}
481-
}
482-
483457
function replaceInArray(nodes: ts.NodeArray<ts.Node>, v: ts.Node, replacement: ts.Node) {
484458
for (let i = 0, len = nodes.length; i < len; ++i) {
485459
if (nodes[i] === v) {
@@ -490,23 +464,6 @@ export function normalizeSourceFile(f: ts.SourceFile, fc: FacadeConverter, expli
490464
}
491465
}
492466

493-
function replaceNode(n: ts.Node, replacement: ts.Node) {
494-
let parent = n.parent;
495-
replacement.parent = parent;
496-
switch (parent.kind) {
497-
case ts.SyntaxKind.ModuleBlock:
498-
let block = <ts.ModuleBlock>parent;
499-
replaceInArray(block.statements, n, replacement);
500-
break;
501-
case ts.SyntaxKind.SourceFile:
502-
let sourceFile = <ts.SourceFile>parent;
503-
replaceInArray(sourceFile.statements, n, replacement);
504-
break;
505-
default:
506-
throw 'replaceNode not implemented for kind:' + parent.kind;
507-
}
508-
}
509-
510467
function gatherClasses(n: ts.Node, classes: Map<string, base.ClassLike>) {
511468
if (ts.isClassExpression(n) || ts.isClassDeclaration(n) || ts.isInterfaceDeclaration(n)) {
512469
let classDecl = <base.ClassLike>n;
@@ -520,13 +477,13 @@ export function normalizeSourceFile(f: ts.SourceFile, fc: FacadeConverter, expli
520477
Array.prototype.push.call(existing.members, e);
521478
e.parent = existing;
522479
});
523-
removeNode(classDecl);
480+
fc.suppressNode(classDecl);
524481
} else {
525482
classes.set(name, classDecl);
526483
// Perform other class level post processing here.
527484
}
528485
} else if (ts.isModuleDeclaration(n) || ts.isSourceFile(n)) {
529-
let moduleClasses: Map<string, base.ClassLike> = new Map();
486+
const moduleClasses: Map<string, base.ClassLike> = new Map();
530487
ts.forEachChild(n, (child) => gatherClasses(child, moduleClasses));
531488
ts.forEachChild(n, (child) => mergeVariablesIntoClasses(child, moduleClasses));
532489
} else if (ts.isModuleBlock(n)) {

test/declaration_test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,31 @@ abstract class XStatic {
580580
}`);
581581
});
582582

583+
it('merges top level variables into classes when they are associated with those classes', () => {
584+
expectTranslate(`interface X { a: number; }
585+
interface Y { c: number; }
586+
587+
declare var X: {prototype: X, new (): X, b: string};
588+
declare var Y: {prototype: Y, new (): Y, d: string};`)
589+
.to.equal(`@JS("X")
590+
abstract class X {
591+
external num get a;
592+
external set a(num v);
593+
external factory X();
594+
external static String get b;
595+
external static set b(String v);
596+
}
597+
598+
@JS("Y")
599+
abstract class Y {
600+
external num get c;
601+
external set c(num v);
602+
external factory Y();
603+
external static String get d;
604+
external static set d(String v);
605+
}`);
606+
});
607+
583608
// If the lib.dom.d.ts file is compiled, it creates conflicting interface definitions which causes
584609
// a problem for variable declarations that we have merged into classes. The main problem was that
585610
// when the declaration transpiler performed the notSimpleBagOfProperties check, it was checking

0 commit comments

Comments
 (0)