Skip to content

Commit 95471f3

Browse files
committed
Move invalid @forRange call error to diagnostics
1 parent f4673a8 commit 95471f3

File tree

6 files changed

+142
-64
lines changed

6 files changed

+142
-64
lines changed

src/transformation/utils/diagnostics.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ const createDiagnosticFactory = <TArgs extends any[]>(
1414
});
1515

1616
export const forbiddenForIn = createDiagnosticFactory(`Iterating over arrays with 'for ... in' is not allowed.`);
17+
18+
export const invalidForRangeCall = createDiagnosticFactory((message: string) => `Invalid @forRange call: ${message}.`);

src/transformation/utils/errors.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,3 @@ export const InvalidAmbientIdentifierName = (node: ts.Identifier) =>
116116
`Invalid ambient identifier name "${node.text}". Ambient identifiers must be valid lua identifiers.`,
117117
node
118118
);
119-
120-
export const InvalidForRangeCall = (node: ts.Node, message: string) =>
121-
new TranspileError(`Invalid @forRange call: ${message}`, node);

src/transformation/visitors/identifier.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as lua from "../../LuaAST";
33
import { transformBuiltinIdentifierExpression } from "../builtins";
44
import { FunctionVisitor, TransformationContext } from "../context";
55
import { isForRangeType } from "../utils/annotations";
6-
import { InvalidForRangeCall } from "../utils/errors";
6+
import { invalidForRangeCall } from "../utils/diagnostics";
77
import { createExportedIdentifier, getIdentifierExportScope } from "../utils/export";
88
import { createSafeName, hasUnsafeIdentifierName } from "../utils/safe-names";
99
import { getIdentifierSymbolId } from "../utils/symbols";
@@ -13,9 +13,8 @@ export function transformIdentifier(context: TransformationContext, identifier:
1313
if (isForRangeType(context, identifier)) {
1414
const callExpression = findFirstNodeAbove(identifier, ts.isCallExpression);
1515
if (!callExpression || !callExpression.parent || !ts.isForOfStatement(callExpression.parent)) {
16-
throw InvalidForRangeCall(
17-
identifier,
18-
"@forRange function can only be used as an iterable in a for...of loop."
16+
context.diagnostics.push(
17+
invalidForRangeCall(identifier, "can be used only as an iterable in a for...of loop")
1918
);
2019
}
2120
}

src/transformation/visitors/loops/for-of.ts

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as ts from "typescript";
22
import * as lua from "../../../LuaAST";
3-
import { cast, castEach } from "../../../utils";
3+
import { assert, cast, castEach } from "../../../utils";
44
import { FunctionVisitor, TransformationContext } from "../../context";
55
import { AnnotationKind, getTypeAnnotations, isForRangeType, isLuaIteratorType } from "../../utils/annotations";
6+
import { invalidForRangeCall } from "../../utils/diagnostics";
67
import {
7-
InvalidForRangeCall,
88
MissingForOfVariables,
99
UnsupportedNonDestructuringLuaIterator,
1010
UnsupportedObjectDestructuringInForOf,
@@ -74,38 +74,50 @@ function transformForRangeStatement(
7474
statement: ts.ForOfStatement,
7575
block: lua.Block
7676
): lua.Statement {
77-
if (!ts.isCallExpression(statement.expression)) {
78-
throw InvalidForRangeCall(statement.expression, "Expression must be a call expression.");
79-
}
77+
assert(ts.isCallExpression(statement.expression));
8078

81-
if (statement.expression.arguments.length < 2 || statement.expression.arguments.length > 3) {
82-
throw InvalidForRangeCall(statement.expression, "@forRange function must take 2 or 3 arguments.");
79+
const callArguments = statement.expression.arguments;
80+
if (callArguments.length !== 2 && callArguments.length !== 3) {
81+
context.diagnostics.push(
82+
invalidForRangeCall(statement.expression, `Expected 2-3 arguments, but got ${callArguments.length}`)
83+
);
8384
}
8485

8586
if (statement.expression.arguments.some(a => !isNumberType(context, context.checker.getTypeAtLocation(a)))) {
86-
throw InvalidForRangeCall(statement.expression, "@forRange arguments must be number types.");
87+
context.diagnostics.push(invalidForRangeCall(statement.expression, "arguments must be numbers"));
8788
}
8889

89-
if (!ts.isVariableDeclarationList(statement.initializer)) {
90-
throw InvalidForRangeCall(statement.initializer, "@forRange loop must declare its own control variable.");
91-
}
90+
const controlVariable = getControlVariable() ?? lua.createAnonymousIdentifier();
91+
function getControlVariable(): lua.Identifier | undefined {
92+
if (!ts.isVariableDeclarationList(statement.initializer)) {
93+
context.diagnostics.push(
94+
invalidForRangeCall(statement.initializer, "loop must declare it's own control variable")
95+
);
96+
return;
97+
}
9298

93-
const controlDeclaration = statement.initializer.declarations[0];
94-
if (!ts.isIdentifier(controlDeclaration.name)) {
95-
throw InvalidForRangeCall(statement.initializer, "@forRange loop cannot use destructuring.");
96-
}
99+
const controlDeclaration = statement.initializer.declarations[0];
100+
if (!ts.isIdentifier(controlDeclaration.name)) {
101+
context.diagnostics.push(invalidForRangeCall(statement.initializer, "destructuring cannot be used"));
102+
return;
103+
}
97104

98-
if (!isNumberType(context, context.checker.getTypeAtLocation(controlDeclaration))) {
99-
throw InvalidForRangeCall(
100-
statement.expression,
101-
"@forRange function must return Iterable<number> or Array<number>."
102-
);
105+
if (!isNumberType(context, context.checker.getTypeAtLocation(controlDeclaration))) {
106+
context.diagnostics.push(
107+
invalidForRangeCall(statement.expression, "function must return Iterable<number>")
108+
);
109+
}
110+
111+
return transformIdentifier(context, controlDeclaration.name);
103112
}
104113

105-
const control = transformIdentifier(context, controlDeclaration.name);
106-
const signature = context.checker.getResolvedSignature(statement.expression);
107-
const [start, limit, step] = transformArguments(context, statement.expression.arguments, signature);
108-
return lua.createForStatement(block, control, start, limit, step, statement);
114+
const [start = lua.createNumericLiteral(0), limit = lua.createNumericLiteral(0), step] = transformArguments(
115+
context,
116+
callArguments,
117+
context.checker.getResolvedSignature(statement.expression)
118+
);
119+
120+
return lua.createForStatement(block, controlVariable, start, limit, step, statement);
109121
}
110122

111123
function transformForOfLuaIteratorStatement(
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`invalid usage argument count ([]): code 1`] = `
4+
"for i = 0, 0 do
5+
end"
6+
`;
7+
8+
exports[`invalid usage argument count ([]): diagnostics 1`] = `"main.ts(6,29): error TSTL: Invalid @forRange call: Expected 2-3 arguments, but got 0."`;
9+
10+
exports[`invalid usage argument count ([1, 2, 3, 4]): code 1`] = `
11+
"for i = 1, 2, 3 do
12+
end"
13+
`;
14+
15+
exports[`invalid usage argument count ([1, 2, 3, 4]): diagnostics 1`] = `"main.ts(6,29): error TSTL: Invalid @forRange call: Expected 2-3 arguments, but got 4."`;
16+
17+
exports[`invalid usage argument count ([1]): code 1`] = `
18+
"for i = 1, 0 do
19+
end"
20+
`;
21+
22+
exports[`invalid usage argument count ([1]): diagnostics 1`] = `"main.ts(6,29): error TSTL: Invalid @forRange call: Expected 2-3 arguments, but got 1."`;
23+
24+
exports[`invalid usage argument types: code 1`] = `
25+
"for i = \\"foo\\", 2 do
26+
end"
27+
`;
28+
29+
exports[`invalid usage argument types: diagnostics 1`] = `"main.ts(6,29): error TSTL: Invalid @forRange call: arguments must be numbers."`;
30+
31+
exports[`invalid usage non-ambient declaration: code 1`] = `
32+
"function luaRange(self)
33+
end"
34+
`;
35+
36+
exports[`invalid usage non-ambient declaration: diagnostics 1`] = `"main.ts(3,22): error TSTL: Invalid @forRange call: can be used only as an iterable in a for...of loop."`;
37+
38+
exports[`invalid usage non-declared loop variable: code 1`] = `
39+
"local i
40+
for ____ = 1, 10, 2 do
41+
end"
42+
`;
43+
44+
exports[`invalid usage non-declared loop variable: diagnostics 1`] = `"main.ts(7,18): error TSTL: Invalid @forRange call: loop must declare it's own control variable."`;
45+
46+
exports[`invalid usage reference ("const call = undefined as any; call(luaRange);"): code 1`] = `
47+
"local call = nil
48+
call(_G, luaRange)"
49+
`;
50+
51+
exports[`invalid usage reference ("const call = undefined as any; call(luaRange);"): diagnostics 1`] = `"main.ts(6,49): error TSTL: Invalid @forRange call: can be used only as an iterable in a for...of loop."`;
52+
53+
exports[`invalid usage reference ("const range = luaRange(1, 10);"): code 1`] = `"local range = luaRange(_G, 1, 10)"`;
54+
55+
exports[`invalid usage reference ("const range = luaRange(1, 10);"): diagnostics 1`] = `"main.ts(6,27): error TSTL: Invalid @forRange call: can be used only as an iterable in a for...of loop."`;
56+
57+
exports[`invalid usage reference ("for (const i of [...luaRange(1, 10)]) {}"): code 1`] = `
58+
"for ____, i in ipairs(
59+
{
60+
table.unpack(
61+
luaRange(_G, 1, 10)
62+
)
63+
}
64+
) do
65+
end"
66+
`;
67+
68+
exports[`invalid usage reference ("for (const i of [...luaRange(1, 10)]) {}"): diagnostics 1`] = `"main.ts(6,33): error TSTL: Invalid @forRange call: can be used only as an iterable in a for...of loop."`;
69+
70+
exports[`invalid usage reference ("let array = [0, luaRange, 1];"): code 1`] = `"local array = {0, luaRange, 1}"`;
71+
72+
exports[`invalid usage reference ("let array = [0, luaRange, 1];"): diagnostics 1`] = `"main.ts(6,29): error TSTL: Invalid @forRange call: can be used only as an iterable in a for...of loop."`;
73+
74+
exports[`invalid usage reference ("luaRange.call(null, 0, 0, 0);"): code 1`] = `
75+
"require(\\"lualib_bundle\\");
76+
__TS__FunctionCall(luaRange, nil, 0, 0, 0)"
77+
`;
78+
79+
exports[`invalid usage reference ("luaRange.call(null, 0, 0, 0);"): diagnostics 1`] = `"main.ts(6,13): error TSTL: Invalid @forRange call: can be used only as an iterable in a for...of loop."`;
80+
81+
exports[`invalid usage return type: code 1`] = `
82+
"for i = 1, 10 do
83+
end"
84+
`;
85+
86+
exports[`invalid usage return type: diagnostics 1`] = `"main.ts(6,29): error TSTL: Invalid @forRange call: function must return Iterable<number>."`;
87+
88+
exports[`invalid usage variable destructuring: code 1`] = `
89+
"for ____ = 1, 10, 2 do
90+
end"
91+
`;
92+
93+
exports[`invalid usage variable destructuring: diagnostics 1`] = `"main.ts(6,18): error TSTL: Invalid @forRange call: destructuring cannot be used."`;
Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as ts from "typescript";
2-
import { InvalidForRangeCall } from "../../../src/transformation/utils/errors";
31
import * as util from "../../util";
42

53
const createForRangeDeclaration = (args = "i: number, j: number, k?: number", returns = "number[]") => `
@@ -30,61 +28,43 @@ describe("invalid usage", () => {
3028
util.testModule`
3129
/** @forRange */
3230
function luaRange() {}
33-
`.expectToHaveDiagnosticOfError(
34-
InvalidForRangeCall(
35-
ts.createEmptyStatement(),
36-
"@forRange function can only be used as an iterable in a for...of loop."
37-
)
38-
);
31+
`.expectDiagnosticsToMatchSnapshot();
3932
});
4033

41-
test.each([[1], [1, 2, 3, 4]])("argument count", args => {
34+
test.each<[number[]]>([[[]], [[1]], [[1, 2, 3, 4]]])("argument count (%p)", args => {
4235
util.testModule`
4336
${createForRangeDeclaration("...args: number[]")}
4437
for (const i of luaRange(${args})) {}
45-
`.expectToHaveDiagnosticOfError(
46-
InvalidForRangeCall(ts.createEmptyStatement(), "@forRange function must take 2 or 3 arguments.")
47-
);
38+
`.expectDiagnosticsToMatchSnapshot();
4839
});
4940

5041
test("non-declared loop variable", () => {
5142
util.testModule`
5243
${createForRangeDeclaration()}
5344
let i: number;
5445
for (i of luaRange(1, 10, 2)) {}
55-
`.expectToHaveDiagnosticOfError(
56-
InvalidForRangeCall(ts.createEmptyStatement(), "@forRange loop must declare its own control variable.")
57-
);
46+
`.expectDiagnosticsToMatchSnapshot();
5847
});
5948

6049
test("argument types", () => {
6150
util.testModule`
6251
${createForRangeDeclaration("i: string, j: number")}
6352
for (const i of luaRange("foo", 2)) {}
64-
`.expectToHaveDiagnosticOfError(
65-
InvalidForRangeCall(ts.createEmptyStatement(), "@forRange arguments must be number types.")
66-
);
53+
`.expectDiagnosticsToMatchSnapshot();
6754
});
6855

6956
test("variable destructuring", () => {
7057
util.testModule`
7158
${createForRangeDeclaration(undefined, "number[][]")}
7259
for (const [i] of luaRange(1, 10, 2)) {}
73-
`.expectToHaveDiagnosticOfError(
74-
InvalidForRangeCall(ts.createEmptyStatement(), "@forRange loop cannot use destructuring.")
75-
);
60+
`.expectDiagnosticsToMatchSnapshot();
7661
});
7762

7863
test("return type", () => {
7964
util.testModule`
8065
${createForRangeDeclaration(undefined, "string[]")}
8166
for (const i of luaRange(1, 10)) {}
82-
`.expectToHaveDiagnosticOfError(
83-
InvalidForRangeCall(
84-
ts.createEmptyStatement(),
85-
"@forRange function must return Iterable<number> or Array<number>."
86-
)
87-
);
67+
`.expectDiagnosticsToMatchSnapshot();
8868
});
8969

9070
test.each([
@@ -97,11 +77,6 @@ describe("invalid usage", () => {
9777
util.testModule`
9878
${createForRangeDeclaration()}
9979
${statement}
100-
`.expectToHaveDiagnosticOfError(
101-
InvalidForRangeCall(
102-
ts.createEmptyStatement(),
103-
"@forRange function can only be used as an iterable in a for...of loop."
104-
)
105-
);
80+
`.expectDiagnosticsToMatchSnapshot();
10681
});
10782
});

0 commit comments

Comments
 (0)