Skip to content

Commit 6db5684

Browse files
authored
Handling exports referenced in different scopes (#618)
* Handling exports referenced in different scopes fixes #617 Exported identifiers referenced in other places were not checking what scope they were declared in when prepending their export table. This led to a notable amount of refactoring to clean up the situation. * simplified transformModuleDeclaration further
1 parent cc5c472 commit 6db5684

File tree

3 files changed

+150
-106
lines changed

3 files changed

+150
-106
lines changed

src/LuaTransformer.ts

Lines changed: 95 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ export class LuaTransformer {
544544
throw TSTLErrors.InvalidExtensionMetaExtension(statement);
545545
}
546546

547-
if ((isExtension || isMetaExtension) && this.isIdentifierExported(className)) {
547+
if ((isExtension || isMetaExtension) && this.getIdentifierExportScope(className) !== undefined) {
548548
// Cannot export extension classes
549549
throw TSTLErrors.InvalidExportsExtension(statement);
550550
}
@@ -772,12 +772,13 @@ export class LuaTransformer {
772772
const classVar = this.createLocalOrExportedOrGlobalDeclaration(className, classTable, statement);
773773
result.push(...classVar);
774774

775-
if (this.isIdentifierExported(className)) {
775+
const exportScope = this.getIdentifierExportScope(className);
776+
if (exportScope) {
776777
// local localClassName = ____exports.className
777778
result.push(
778779
tstl.createVariableDeclarationStatement(
779780
localClassName,
780-
this.addExportToIdentifier(tstl.cloneIdentifier(className))
781+
this.createExportedIdentifier(tstl.cloneIdentifier(className), exportScope)
781782
)
782783
);
783784
}
@@ -1500,9 +1501,7 @@ export class LuaTransformer {
15001501
: tstl.createTableIndexExpression(tableExpression, tstl.createNumericLiteral(index + 1));
15011502
result.push(...this.createLocalOrExportedOrGlobalDeclaration(variableName, expression));
15021503
if (element.initializer) {
1503-
const identifier = this.shouldExportIdentifier(variableName)
1504-
? this.createExportedIdentifier(variableName)
1505-
: variableName;
1504+
const identifier = this.addExportToIdentifier(variableName);
15061505
result.push(
15071506
tstl.createIfStatement(
15081507
tstl.createBinaryExpression(
@@ -1561,57 +1560,23 @@ export class LuaTransformer {
15611560
const nameIdentifier = this.transformIdentifier(statement.name as ts.Identifier);
15621561

15631562
if (isFirstDeclaration) {
1564-
const isExported = (ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export) !== 0;
1565-
if (isExported && this.currentNamespace) {
1566-
// outerNS.innerNS = {}
1567-
const namespaceDeclaration = tstl.createAssignmentStatement(
1568-
tstl.createTableIndexExpression(
1569-
this.createModuleLocalNameIdentifier(this.currentNamespace),
1570-
tstl.createStringLiteral(nameIdentifier.text)
1571-
),
1572-
tstl.createTableExpression()
1573-
);
1574-
1575-
result.push(namespaceDeclaration);
1576-
1577-
if (hasExports && tsHelper.moduleHasEmittedBody(statement)) {
1578-
// local innerNS = outerNS.innerNS
1579-
const localDeclaration = this.createHoistableVariableDeclarationStatement(
1580-
this.createModuleLocalNameIdentifier(statement),
1581-
tstl.createTableIndexExpression(
1582-
this.createModuleLocalNameIdentifier(this.currentNamespace),
1583-
tstl.createStringLiteral(nameIdentifier.text)
1584-
)
1585-
);
1586-
1587-
result.push(localDeclaration);
1588-
}
1589-
} else if (isExported && !this.currentNamespace && this.isModule) {
1590-
// exports.NS = {}
1591-
const namespaceDeclaration = tstl.createAssignmentStatement(
1592-
this.createExportedIdentifier(nameIdentifier),
1593-
tstl.createTableExpression()
1594-
);
1595-
1596-
result.push(namespaceDeclaration);
1563+
// local NS = {} or exportTable.NS = {}
1564+
const localDeclaration = this.createLocalOrExportedOrGlobalDeclaration(
1565+
nameIdentifier,
1566+
tstl.createTableExpression()
1567+
);
15971568

1598-
if (hasExports && tsHelper.moduleHasEmittedBody(statement)) {
1599-
// local NS = exports.NS
1600-
const localDeclaration = this.createHoistableVariableDeclarationStatement(
1601-
this.createModuleLocalNameIdentifier(statement),
1602-
this.createExportedIdentifier(tstl.cloneIdentifier(nameIdentifier, statement.name))
1603-
);
1569+
result.push(...localDeclaration);
16041570

1605-
result.push(localDeclaration);
1606-
}
1607-
} else {
1608-
// local NS = {}
1609-
const localDeclaration = this.createLocalOrExportedOrGlobalDeclaration(
1571+
const exportScope = this.getIdentifierExportScope(nameIdentifier);
1572+
if (exportScope && hasExports && tsHelper.moduleHasEmittedBody(statement)) {
1573+
// local NS = exportTable.NS
1574+
const localDeclaration = this.createHoistableVariableDeclarationStatement(
16101575
this.createModuleLocalNameIdentifier(statement),
1611-
tstl.createTableExpression()
1576+
this.createExportedIdentifier(nameIdentifier, exportScope)
16121577
);
16131578

1614-
result.push(...localDeclaration);
1579+
result.push(localDeclaration);
16151580
}
16161581
}
16171582

@@ -1985,9 +1950,7 @@ export class LuaTransformer {
19851950
statement.name.elements.forEach(element => {
19861951
if (!ts.isOmittedExpression(element) && element.initializer) {
19871952
const variableName = this.transformIdentifier(element.name as ts.Identifier);
1988-
const identifier = this.shouldExportIdentifier(variableName)
1989-
? this.createExportedIdentifier(variableName)
1990-
: variableName;
1953+
const identifier = this.addExportToIdentifier(variableName);
19911954
statements.push(
19921955
tstl.createIfStatement(
19931956
tstl.createBinaryExpression(
@@ -3347,10 +3310,7 @@ export class LuaTransformer {
33473310
properties.push(tstl.createTableFieldExpression(expression, name, element));
33483311
} else if (ts.isShorthandPropertyAssignment(element)) {
33493312
const valueSymbol = this.checker.getShorthandAssignmentValueSymbol(element);
3350-
let identifier = this.createShorthandIdentifier(valueSymbol, element.name);
3351-
if (tstl.isIdentifier(identifier) && valueSymbol !== undefined && this.isSymbolExported(valueSymbol)) {
3352-
identifier = this.createExportedIdentifier(identifier);
3353-
}
3313+
const identifier = this.createShorthandIdentifier(valueSymbol, element.name);
33543314
properties.push(tstl.createTableFieldExpression(identifier, name, element));
33553315
} else if (ts.isMethodDeclaration(element)) {
33563316
const expression = this.transformFunctionExpression(element);
@@ -4640,8 +4600,10 @@ export class LuaTransformer {
46404600

46414601
private transformIdentifierExpression(expression: ts.Identifier): tstl.Expression {
46424602
const identifier = this.transformIdentifier(expression);
4643-
if (this.isIdentifierExported(identifier)) {
4644-
return this.createExportedIdentifier(identifier);
4603+
4604+
const exportScope = this.getIdentifierExportScope(identifier);
4605+
if (exportScope) {
4606+
return this.createExportedIdentifier(identifier, exportScope);
46454607
}
46464608

46474609
switch (this.getIdentifierText(expression)) {
@@ -4664,28 +4626,44 @@ export class LuaTransformer {
46644626
return identifier;
46654627
}
46664628

4667-
protected isIdentifierExported(identifier: tstl.Identifier): boolean {
4668-
const symbolInfo = identifier.symbolId && this.symbolInfo.get(identifier.symbolId);
4669-
if (!symbolInfo) {
4670-
return false;
4629+
protected getSymbolFromIdentifier(identifier: tstl.Identifier): ts.Symbol | undefined {
4630+
if (identifier.symbolId !== undefined) {
4631+
const symbolInfo = this.symbolInfo.get(identifier.symbolId);
4632+
if (symbolInfo !== undefined) {
4633+
return symbolInfo.symbol;
4634+
}
4635+
}
4636+
return undefined;
4637+
}
4638+
4639+
protected getIdentifierExportScope(identifier: tstl.Identifier): ts.SourceFile | ts.ModuleDeclaration | undefined {
4640+
const symbol = this.getSymbolFromIdentifier(identifier);
4641+
if (!symbol) {
4642+
return undefined;
46714643
}
46724644

4673-
return this.isSymbolExported(symbolInfo.symbol);
4645+
return this.getSymbolExportScope(symbol);
46744646
}
46754647

46764648
protected isSymbolExported(symbol: ts.Symbol): boolean {
4677-
if (!this.isModule && !this.currentNamespace) {
4649+
if (tsHelper.getExportedSymbolDeclaration(symbol) !== undefined) {
4650+
return true;
4651+
} else if (this.currentSourceFile) {
4652+
// Symbol may have been exported separately (e.g. 'const foo = "bar"; export { foo }')
4653+
return this.isSymbolExportedFromScope(symbol, this.currentSourceFile);
4654+
} else {
46784655
return false;
46794656
}
4657+
}
46804658

4681-
const currentScope = this.currentNamespace ? this.currentNamespace : this.currentSourceFile;
4682-
if (currentScope === undefined) {
4683-
throw TSTLErrors.UndefinedScope();
4659+
protected isSymbolExportedFromScope(symbol: ts.Symbol, scope: ts.SourceFile | ts.ModuleDeclaration): boolean {
4660+
if (ts.isSourceFile(scope) && !tsHelper.isFileModule(scope)) {
4661+
return false;
46844662
}
46854663

4686-
let scopeSymbol = this.checker.getSymbolAtLocation(currentScope);
4664+
let scopeSymbol = this.checker.getSymbolAtLocation(scope);
46874665
if (scopeSymbol === undefined) {
4688-
scopeSymbol = this.checker.getTypeAtLocation(currentScope).getSymbol();
4666+
scopeSymbol = this.checker.getTypeAtLocation(scope).getSymbol();
46894667
}
46904668

46914669
if (scopeSymbol === undefined || scopeSymbol.exports === undefined) {
@@ -4705,25 +4683,43 @@ export class LuaTransformer {
47054683
}
47064684

47074685
protected addExportToIdentifier(identifier: tstl.Identifier): tstl.AssignmentLeftHandSideExpression {
4708-
if (this.isIdentifierExported(identifier)) {
4709-
return this.createExportedIdentifier(identifier);
4686+
const exportScope = this.getIdentifierExportScope(identifier);
4687+
if (exportScope) {
4688+
return this.createExportedIdentifier(identifier, exportScope);
47104689
}
47114690
return identifier;
47124691
}
47134692

4714-
protected createExportedIdentifier(identifier: tstl.Identifier): tstl.TableIndexExpression {
4715-
let exportTable: tstl.Identifier;
4716-
if (this.currentNamespace !== undefined) {
4717-
if (this.isUnsafeName(this.currentNamespace.name.text)) {
4718-
exportTable = this.createModuleLocalNameIdentifier(this.currentNamespace);
4719-
} else {
4720-
exportTable = this.transformIdentifier(this.currentNamespace.name as ts.Identifier);
4721-
}
4722-
} else {
4723-
exportTable = this.createExportsIdentifier();
4693+
protected createExportedIdentifier(
4694+
identifier: tstl.Identifier,
4695+
exportScope?: ts.SourceFile | ts.ModuleDeclaration
4696+
): tstl.AssignmentLeftHandSideExpression {
4697+
const exportTable =
4698+
exportScope && ts.isModuleDeclaration(exportScope)
4699+
? this.createModuleLocalNameIdentifier(exportScope)
4700+
: this.createExportsIdentifier();
4701+
return tstl.createTableIndexExpression(exportTable, tstl.createStringLiteral(identifier.text));
4702+
}
4703+
4704+
protected getSymbolExportScope(symbol: ts.Symbol): ts.SourceFile | ts.ModuleDeclaration | undefined {
4705+
const exportedDeclaration = tsHelper.getExportedSymbolDeclaration(symbol);
4706+
if (!exportedDeclaration) {
4707+
return undefined;
47244708
}
47254709

4726-
return tstl.createTableIndexExpression(exportTable, tstl.createStringLiteral(identifier.text));
4710+
const scope = tsHelper.findFirstNodeAbove(
4711+
exportedDeclaration,
4712+
(n): n is ts.SourceFile | ts.ModuleDeclaration => ts.isSourceFile(n) || ts.isModuleDeclaration(n)
4713+
);
4714+
if (!scope) {
4715+
return undefined;
4716+
}
4717+
4718+
if (!this.isSymbolExportedFromScope(symbol, scope)) {
4719+
return undefined;
4720+
}
4721+
4722+
return scope;
47274723
}
47284724

47294725
protected transformLuaLibFunction(
@@ -4827,17 +4823,6 @@ export class LuaTransformer {
48274823
return filePath.replace(/\.\//g, "").replace(/\//g, ".");
48284824
}
48294825

4830-
protected shouldExportIdentifier(identifier: tstl.Identifier | tstl.Identifier[]): boolean {
4831-
if (!this.isModule && !this.currentNamespace) {
4832-
return false;
4833-
}
4834-
if (Array.isArray(identifier)) {
4835-
return identifier.some(i => this.isIdentifierExported(i));
4836-
} else {
4837-
return this.isIdentifierExported(identifier);
4838-
}
4839-
}
4840-
48414826
protected createSelfIdentifier(tsOriginal?: ts.Node): tstl.Identifier {
48424827
return tstl.createIdentifier("self", tsOriginal);
48434828
}
@@ -4857,20 +4842,19 @@ export class LuaTransformer {
48574842

48584843
const functionDeclaration = tsOriginal && ts.isFunctionDeclaration(tsOriginal) ? tsOriginal : undefined;
48594844

4860-
if (this.shouldExportIdentifier(lhs)) {
4845+
const identifiers = Array.isArray(lhs) ? lhs : [lhs];
4846+
if (identifiers.length === 0) {
4847+
return [];
4848+
}
4849+
4850+
const exportScope = this.getIdentifierExportScope(identifiers[0]);
4851+
if (exportScope) {
48614852
// exported
48624853
if (!rhs) {
48634854
return [];
4864-
} else if (Array.isArray(lhs)) {
4865-
assignment = tstl.createAssignmentStatement(
4866-
lhs.map(i => this.createExportedIdentifier(i)),
4867-
rhs,
4868-
tsOriginal,
4869-
parent
4870-
);
48714855
} else {
48724856
assignment = tstl.createAssignmentStatement(
4873-
this.createExportedIdentifier(lhs),
4857+
identifiers.map(i => this.createExportedIdentifier(i, exportScope)),
48744858
rhs,
48754859
tsOriginal,
48764860
parent
@@ -5120,10 +5104,15 @@ export class LuaTransformer {
51205104
name = this.hasUnsafeIdentifierName(propertyIdentifier) ? this.createSafeName(propertyName) : propertyName;
51215105
}
51225106

5123-
const identifier = this.transformIdentifierExpression(ts.createIdentifier(name));
5107+
let identifier = this.transformIdentifierExpression(ts.createIdentifier(name));
51245108
tstl.setNodeOriginal(identifier, propertyIdentifier);
51255109
if (valueSymbol !== undefined && tstl.isIdentifier(identifier)) {
51265110
identifier.symbolId = this.symbolIds.get(valueSymbol);
5111+
5112+
const exportScope = this.getSymbolExportScope(valueSymbol);
5113+
if (exportScope) {
5114+
identifier = this.createExportedIdentifier(identifier, exportScope);
5115+
}
51275116
}
51285117
return identifier;
51295118
}

src/TSHelper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ export class TSHelper {
7272
);
7373
}
7474

75+
public static getExportedSymbolDeclaration(symbol: ts.Symbol): ts.Declaration | undefined {
76+
const declarations = symbol.getDeclarations();
77+
if (declarations) {
78+
return declarations.find(d => (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Export) !== 0);
79+
}
80+
return undefined;
81+
}
82+
7583
public static isDeclaration(node: ts.Node): node is ts.Declaration {
7684
return (
7785
ts.isEnumDeclaration(node) ||

test/unit/identifiers.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,53 @@ test.each(validTsInvalidLuaNames)("exported values with invalid lua identifier n
148148
expect(util.executeLua(`return (function() ${lua} end)()["${name}"]`)).toBe("foobar");
149149
});
150150

151+
test("exported identifiers referenced in namespace (%p)", () => {
152+
const code = `
153+
export const foo = "foobar";
154+
namespace NS {
155+
export const bar = foo;
156+
}
157+
export const baz = NS.bar;`;
158+
expect(util.transpileExecuteAndReturnExport(code, "baz")).toBe("foobar");
159+
});
160+
161+
test("exported namespace identifiers referenced in different namespace (%p)", () => {
162+
const tsHeader = `
163+
namespace A {
164+
export const foo = "foobar";
165+
namespace B {
166+
export const bar = foo;
167+
}
168+
export const baz = B.bar;
169+
}`;
170+
expect(util.transpileAndExecute("return A.baz", undefined, undefined, tsHeader)).toBe("foobar");
171+
});
172+
173+
test("exported identifiers referenced in nested scope (%p)", () => {
174+
const code = `
175+
export const foo = "foobar";
176+
namespace A {
177+
export namespace B {
178+
export const bar = foo;
179+
}
180+
}
181+
export const baz = A.B.bar;`;
182+
expect(util.transpileExecuteAndReturnExport(code, "baz")).toBe("foobar");
183+
});
184+
185+
test.each(validTsInvalidLuaNames)(
186+
"exported values with invalid lua identifier names referenced in different scope (%p)",
187+
name => {
188+
const code = `
189+
export const ${name} = "foobar";
190+
namespace NS {
191+
export const foo = ${name};
192+
}
193+
export const bar = NS.foo;`;
194+
expect(util.transpileExecuteAndReturnExport(code, "bar")).toBe("foobar");
195+
}
196+
);
197+
151198
test.each(validTsInvalidLuaNames)("class with invalid lua name has correct name property", name => {
152199
const code = `
153200
class ${name} {}

0 commit comments

Comments
 (0)