Skip to content

Commit 61d0738

Browse files
authored
Warning if truthiness (#1333)
* Add warning diagnostic when using condition variable that will never be false in Lua * Fix tests * Fix ugly import
1 parent f822858 commit 61d0738

File tree

5 files changed

+77
-3
lines changed

5 files changed

+77
-3
lines changed

src/transformation/utils/diagnostics.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ export const annotationDeprecated = createWarningDiagnosticFactory(
147147
`See https://typescripttolua.github.io/docs/advanced/compiler-annotations#${kind.toLowerCase()} for more information.`
148148
);
149149

150+
export const truthyOnlyConditionalValue = createWarningDiagnosticFactory(
151+
"Numbers and strings will always evaluate to true in Lua. Explicitly check the value with ===."
152+
);
153+
150154
export const notAllowedOptionalAssignment = createErrorDiagnosticFactory(
151155
"The left-hand side of an assignment expression may not be an optional property access."
152156
);

src/transformation/visitors/conditional.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { transformInPrecedingStatementScope } from "../utils/preceding-statement
55
import { performHoisting, ScopeType } from "../utils/scope";
66
import { transformBlockOrStatement } from "./block";
77
import { canBeFalsy } from "../utils/typescript";
8+
import { truthyOnlyConditionalValue } from "../utils/diagnostics";
89

910
type EvaluatedExpression = [precedingStatemens: lua.Statement[], value: lua.Expression];
1011

@@ -39,6 +40,9 @@ function transformProtectedConditionalExpression(
3940
}
4041

4142
export const transformConditionalExpression: FunctionVisitor<ts.ConditionalExpression> = (expression, context) => {
43+
// Check if we need to add diagnostic about Lua truthiness
44+
checkOnlyTruthyCondition(expression.condition, context);
45+
4246
const condition = transformInPrecedingStatementScope(context, () =>
4347
context.transformExpression(expression.condition)
4448
);
@@ -64,6 +68,10 @@ export const transformConditionalExpression: FunctionVisitor<ts.ConditionalExpre
6468

6569
export function transformIfStatement(statement: ts.IfStatement, context: TransformationContext): lua.IfStatement {
6670
context.pushScope(ScopeType.Conditional);
71+
72+
// Check if we need to add diagnostic about Lua truthiness
73+
checkOnlyTruthyCondition(statement.expression, context);
74+
6775
const condition = context.transformExpression(statement.expression);
6876
const statements = performHoisting(context, transformBlockOrStatement(context, statement.thenStatement));
6977
context.popScope();
@@ -103,3 +111,9 @@ export function transformIfStatement(statement: ts.IfStatement, context: Transfo
103111

104112
return lua.createIfStatement(condition, ifBlock);
105113
}
114+
115+
export function checkOnlyTruthyCondition(condition: ts.Expression, context: TransformationContext) {
116+
if (!canBeFalsy(context, context.checker.getTypeAtLocation(condition))) {
117+
context.diagnostics.push(truthyOnlyConditionalValue(condition));
118+
}
119+
}

src/transformation/visitors/loops/do-while.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import * as ts from "typescript";
22
import * as lua from "../../../LuaAST";
33
import { FunctionVisitor } from "../../context";
44
import { transformInPrecedingStatementScope } from "../../utils/preceding-statements";
5+
import { checkOnlyTruthyCondition } from "../conditional";
56
import { invertCondition, transformLoopBody } from "./utils";
67

78
export const transformWhileStatement: FunctionVisitor<ts.WhileStatement> = (statement, context) => {
9+
// Check if we need to add diagnostic about Lua truthiness
10+
checkOnlyTruthyCondition(statement.expression, context);
11+
812
const body = transformLoopBody(context, statement);
913

1014
let [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () =>
@@ -37,6 +41,9 @@ export const transformWhileStatement: FunctionVisitor<ts.WhileStatement> = (stat
3741
};
3842

3943
export const transformDoStatement: FunctionVisitor<ts.DoStatement> = (statement, context) => {
44+
// Check if we need to add diagnostic about Lua truthiness
45+
checkOnlyTruthyCondition(statement.expression, context);
46+
4047
const body = lua.createDoStatement(transformLoopBody(context, statement));
4148

4249
let [conditionPrecedingStatements, condition] = transformInPrecedingStatementScope(context, () =>

test/unit/conditionals.spec.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as tstl from "../../src";
22
import * as util from "../util";
3+
import { truthyOnlyConditionalValue } from "../../src/transformation/utils/diagnostics";
34

45
test.each([0, 1])("if (%p)", inp => {
56
util.testFunction`
@@ -88,7 +89,9 @@ test.each([
8889
{ condition: false, lhs: 4, rhs: 5 },
8990
{ condition: 3, lhs: 4, rhs: 5 },
9091
])("Ternary Conditional (%p)", ({ condition, lhs, rhs }) => {
91-
util.testExpressionTemplate`${condition} ? ${lhs} : ${rhs}`.expectToMatchJsResult();
92+
util.testExpressionTemplate`${condition} ? ${lhs} : ${rhs}`
93+
.ignoreDiagnostics([truthyOnlyConditionalValue.code])
94+
.expectToMatchJsResult();
9295
});
9396

9497
test.each(["true", "false", "a < 4", "a == 8"])("Ternary Conditional Delayed (%p)", condition => {
@@ -138,3 +141,49 @@ test.each([false, true])("Ternary conditional with preceding statements in false
138141
})
139142
.expectToMatchJsResult();
140143
});
144+
145+
test.each(["string", "number", "string | number"])(
146+
"Warning when using if statement that cannot evaluate to false undefined or null (%p)",
147+
type => {
148+
util.testFunction`
149+
if (condition) {}
150+
`
151+
.setTsHeader(`declare var condition: ${type};`)
152+
.setOptions({ strict: true })
153+
.expectToHaveDiagnostics([truthyOnlyConditionalValue.code]);
154+
}
155+
);
156+
157+
test.each(["string", "number", "string | number"])(
158+
"Warning when using while statement that cannot evaluate to false undefined or null (%p)",
159+
type => {
160+
util.testFunction`
161+
while (condition) {}
162+
`
163+
.setTsHeader(`declare var condition: ${type};`)
164+
.setOptions({ strict: true })
165+
.expectToHaveDiagnostics([truthyOnlyConditionalValue.code]);
166+
}
167+
);
168+
169+
test.each(["string", "number", "string | number"])(
170+
"Warning when using do while statement that cannot evaluate to false undefined or null (%p)",
171+
type => {
172+
util.testFunction`
173+
do {} while (condition)
174+
`
175+
.setTsHeader(`declare var condition: ${type};`)
176+
.setOptions({ strict: true })
177+
.expectToHaveDiagnostics([truthyOnlyConditionalValue.code]);
178+
}
179+
);
180+
181+
test.each(["string", "number", "string | number"])(
182+
"Warning when using ternary that cannot evaluate to false undefined or null (%p)",
183+
type => {
184+
util.testExpression`condition ? 1 : 0`
185+
.setTsHeader(`declare var condition: ${type};`)
186+
.setOptions({ strict: true })
187+
.expectToHaveDiagnostics([truthyOnlyConditionalValue.code]);
188+
}
189+
);

test/unit/identifiers.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => {
302302

303303
test("variable (elseif)", () => {
304304
util.testFunction`
305-
const elseif = "foobar";
305+
const elseif: string | undefined = "foobar";
306306
if (false) {
307307
} else if (elseif) {
308308
return elseif;
@@ -350,7 +350,7 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => {
350350

351351
test("variable (then)", () => {
352352
util.testFunction`
353-
const then = "foobar";
353+
const then: string | undefined = "foobar";
354354
if (then) {
355355
return then;
356356
}

0 commit comments

Comments
 (0)