Skip to content

Commit a940c71

Browse files
authored
@forRange directive (#626)
* @forRange directive * throwing error on all references to @forRange function outside for...of expression
1 parent 46482c7 commit a940c71

File tree

5 files changed

+180
-1
lines changed

5 files changed

+180
-1
lines changed

src/Decorator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export class Decorator {
2929
return DecoratorKind.NoSelf;
3030
case "noselfinfile":
3131
return DecoratorKind.NoSelfInFile;
32+
case "forrange":
33+
return DecoratorKind.ForRange;
3234
}
3335

3436
return undefined;
@@ -61,4 +63,5 @@ export enum DecoratorKind {
6163
LuaTable = "LuaTable",
6264
NoSelf = "NoSelf",
6365
NoSelfInFile = "NoSelfInFile",
66+
ForRange = "ForRange",
6467
}

src/LuaTransformer.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2379,11 +2379,59 @@ export class LuaTransformer {
23792379
}
23802380
}
23812381

2382+
protected transformForRangeStatement(statement: ts.ForOfStatement, body: tstl.Block): StatementVisitResult {
2383+
if (!ts.isCallExpression(statement.expression)) {
2384+
throw TSTLErrors.InvalidForRangeCall(statement.expression, "Expression must be a call expression.");
2385+
}
2386+
2387+
if (statement.expression.arguments.length < 2 || statement.expression.arguments.length > 3) {
2388+
throw TSTLErrors.InvalidForRangeCall(
2389+
statement.expression,
2390+
"@forRange function must take 2 or 3 arguments."
2391+
);
2392+
}
2393+
2394+
if (statement.expression.arguments.some(a => !tsHelper.isNumberType(this.checker.getTypeAtLocation(a)))) {
2395+
throw TSTLErrors.InvalidForRangeCall(statement.expression, "@forRange arguments must be number types.");
2396+
}
2397+
2398+
if (!ts.isVariableDeclarationList(statement.initializer)) {
2399+
throw TSTLErrors.InvalidForRangeCall(
2400+
statement.initializer,
2401+
"@forRange loop must declare its own control variable."
2402+
);
2403+
}
2404+
2405+
const controlDeclaration = statement.initializer.declarations[0];
2406+
if (!ts.isIdentifier(controlDeclaration.name)) {
2407+
throw TSTLErrors.InvalidForRangeCall(statement.initializer, "@forRange loop cannot use destructuring.");
2408+
}
2409+
2410+
const controlType = this.checker.getTypeAtLocation(controlDeclaration);
2411+
if (controlType && !tsHelper.isNumberType(controlType)) {
2412+
throw TSTLErrors.InvalidForRangeCall(
2413+
statement.expression,
2414+
"@forRange function must return Iterable<number> or Array<number>."
2415+
);
2416+
}
2417+
2418+
const control = this.transformIdentifier(controlDeclaration.name);
2419+
const signature = this.checker.getResolvedSignature(statement.expression);
2420+
const [start, limit, step] = this.transformArguments(statement.expression.arguments, signature);
2421+
return tstl.createForStatement(body, control, start, limit, step, statement);
2422+
}
2423+
23822424
public transformForOfStatement(statement: ts.ForOfStatement): StatementVisitResult {
23832425
// Transpile body
23842426
const body = tstl.createBlock(this.transformLoopBody(statement));
23852427

2386-
if (tsHelper.isLuaIteratorType(statement.expression, this.checker)) {
2428+
if (
2429+
ts.isCallExpression(statement.expression) &&
2430+
tsHelper.isForRangeType(statement.expression.expression, this.checker)
2431+
) {
2432+
// ForRange
2433+
return this.transformForRangeStatement(statement, body);
2434+
} else if (tsHelper.isLuaIteratorType(statement.expression, this.checker)) {
23872435
// LuaIterators
23882436
return this.transformForOfLuaIteratorStatement(statement, body);
23892437
} else if (
@@ -4638,6 +4686,16 @@ export class LuaTransformer {
46384686
return tstl.createIdentifier("nil");
46394687
}
46404688

4689+
if (tsHelper.isForRangeType(identifier, this.checker)) {
4690+
const callExpression = tsHelper.findFirstNodeAbove(identifier, ts.isCallExpression);
4691+
if (!callExpression || !callExpression.parent || !ts.isForOfStatement(callExpression.parent)) {
4692+
throw TSTLErrors.InvalidForRangeCall(
4693+
identifier,
4694+
"@forRange function can only be used as an iterable in a for...of loop."
4695+
);
4696+
}
4697+
}
4698+
46414699
const text = this.hasUnsafeIdentifierName(identifier)
46424700
? this.createSafeName(this.getIdentifierText(identifier))
46434701
: this.getIdentifierText(identifier);

src/TSHelper.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ export class TSHelper {
184184
return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.LuaIterator);
185185
}
186186

187+
public static isForRangeType(node: ts.Node, checker: ts.TypeChecker): boolean {
188+
const type = checker.getTypeAtLocation(node);
189+
return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.ForRange);
190+
}
191+
187192
public static isTupleReturnCall(node: ts.Node, checker: ts.TypeChecker): boolean {
188193
if (ts.isCallExpression(node)) {
189194
const signature = checker.getResolvedSignature(node);

src/TSTLErrors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,8 @@ export class TSTLErrors {
204204
node
205205
);
206206
};
207+
208+
public static InvalidForRangeCall = (node: ts.Node, message: string) => {
209+
return new TranspileError(`Invalid @forRange call: ${message}`, node);
210+
};
207211
}

test/unit/loops.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,3 +797,112 @@ test("while dead code after return", () => {
797797

798798
expect(result).toBe(3);
799799
});
800+
801+
test.each([
802+
{ args: [1, 10], expectResult: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] },
803+
{ args: [1, 10, 2], expectResult: [1, 3, 5, 7, 9] },
804+
{ args: [10, 1, -1], expectResult: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] },
805+
{ args: [10, 1, -2], expectResult: [10, 8, 6, 4, 2] },
806+
])("@forRange loop", ({ args, expectResult }) => {
807+
const tsHeader = "/** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[];";
808+
const code = `
809+
const results: number[] = [];
810+
for (const i of luaRange(${args})) {
811+
results.push(i);
812+
}
813+
return JSONStringify(results);`;
814+
815+
const result = util.transpileAndExecute(code, undefined, undefined, tsHeader);
816+
expect(JSON.parse(result)).toEqual(expectResult);
817+
});
818+
819+
test("invalid non-ambient @forRange function", () => {
820+
const code = `
821+
/** @forRange **/ function luaRange(i: number, j: number, k?: number): number[] { return []; }
822+
for (const i of luaRange(1, 10, 2)) {}`;
823+
824+
expect(() => util.transpileString(code)).toThrow(
825+
TSTLErrors.InvalidForRangeCall(
826+
ts.createEmptyStatement(),
827+
"@forRange function can only be used as an iterable in a for...of loop."
828+
).message
829+
);
830+
});
831+
832+
test.each([[1], [1, 2, 3, 4]])("invalid @forRange argument count", args => {
833+
const code = `
834+
/** @forRange **/ declare function luaRange(...args: number[]): number[] { return []; }
835+
for (const i of luaRange(${args})) {}`;
836+
837+
expect(() => util.transpileString(code)).toThrow(
838+
TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange function must take 2 or 3 arguments.")
839+
.message
840+
);
841+
});
842+
843+
test("invalid @forRange control variable", () => {
844+
const code = `
845+
/** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[];
846+
let i: number;
847+
for (i of luaRange(1, 10, 2)) {}`;
848+
849+
expect(() => util.transpileString(code)).toThrow(
850+
TSTLErrors.InvalidForRangeCall(
851+
ts.createEmptyStatement(),
852+
"@forRange loop must declare its own control variable."
853+
).message
854+
);
855+
});
856+
857+
test("invalid @forRange argument type", () => {
858+
const code = `
859+
/** @forRange **/ declare function luaRange(i: string, j: number): number[] { return []; }
860+
for (const i of luaRange("foo", 2)) {}`;
861+
862+
expect(() => util.transpileString(code)).toThrow(
863+
TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange arguments must be number types.").message
864+
);
865+
});
866+
867+
test("invalid @forRange destructuring", () => {
868+
const code = `
869+
/** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[][];
870+
for (const [i] of luaRange(1, 10, 2)) {}`;
871+
872+
expect(() => util.transpileString(code)).toThrow(
873+
TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange loop cannot use destructuring.").message
874+
);
875+
});
876+
877+
test("invalid @forRange return type", () => {
878+
const code = `
879+
/** @forRange **/ declare function luaRange(i: number, j: number, k?: number): string[];
880+
for (const i of luaRange(1, 10)) {}`;
881+
882+
expect(() => util.transpileString(code)).toThrow(
883+
TSTLErrors.InvalidForRangeCall(
884+
ts.createEmptyStatement(),
885+
"@forRange function must return Iterable<number> or Array<number>."
886+
).message
887+
);
888+
});
889+
890+
test.each([
891+
"const range = luaRange(1, 10);",
892+
"console.log(luaRange);",
893+
"luaRange.call(null, 0, 0, 0);",
894+
"let array = [0, luaRange, 1];",
895+
"const call: any; call(luaRange);",
896+
"for (const i of [...luaRange(1, 10)]) {}",
897+
])("invalid @forRange reference (%p)", statement => {
898+
const code = `
899+
/** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[];
900+
${statement}`;
901+
902+
expect(() => util.transpileString(code)).toThrow(
903+
TSTLErrors.InvalidForRangeCall(
904+
ts.createEmptyStatement(),
905+
"@forRange function can only be used as an iterable in a for...of loop."
906+
).message
907+
);
908+
});

0 commit comments

Comments
 (0)