Skip to content

Commit e002159

Browse files
authored
feat(49962): Disallow comparison against NaN (#50626)
* feat(49962): disallow comparison against NaN * change diagnostic message * use global NaN symbol for NaN equality comparisons
1 parent 23746af commit e002159

17 files changed

+651
-10
lines changed

src/compiler/checker.ts

+29
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,7 @@ namespace ts {
999999
let deferredGlobalOmitSymbol: Symbol | undefined;
10001000
let deferredGlobalAwaitedSymbol: Symbol | undefined;
10011001
let deferredGlobalBigIntType: ObjectType | undefined;
1002+
let deferredGlobalNaNSymbol: Symbol | undefined;
10021003
let deferredGlobalRecordSymbol: Symbol | undefined;
10031004

10041005
const allPotentiallyUnusedIdentifiers = new Map<Path, PotentiallyUnusedIdentifier[]>(); // key is file name
@@ -14343,6 +14344,10 @@ namespace ts {
1434314344
return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as __String, /*arity*/ 0, /*reportErrors*/ false)) || emptyObjectType;
1434414345
}
1434514346

14347+
function getGlobalNaNSymbol(): Symbol | undefined {
14348+
return (deferredGlobalNaNSymbol ||= getGlobalValueSymbol("NaN" as __String, /*reportErrors*/ false));
14349+
}
14350+
1434614351
function getGlobalRecordSymbol(): Symbol | undefined {
1434714352
deferredGlobalRecordSymbol ||= getGlobalTypeAliasSymbol("Record" as __String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol;
1434814353
return deferredGlobalRecordSymbol === unknownSymbol ? undefined : deferredGlobalRecordSymbol;
@@ -34495,6 +34500,7 @@ namespace ts {
3449534500
const eqType = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken;
3449634501
error(errorNode, Diagnostics.This_condition_will_always_return_0_since_JavaScript_compares_objects_by_reference_not_value, eqType ? "false" : "true");
3449734502
}
34503+
checkNaNEquality(errorNode, operator, left, right);
3449834504
reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left));
3449934505
return booleanType;
3450034506

@@ -34727,6 +34733,29 @@ namespace ts {
3472734733
return undefined;
3472834734
}
3472934735
}
34736+
34737+
function checkNaNEquality(errorNode: Node | undefined, operator: SyntaxKind, left: Expression, right: Expression) {
34738+
const isLeftNaN = isGlobalNaN(skipParentheses(left));
34739+
const isRightNaN = isGlobalNaN(skipParentheses(right));
34740+
if (isLeftNaN || isRightNaN) {
34741+
const err = error(errorNode, Diagnostics.This_condition_will_always_return_0,
34742+
tokenToString(operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.EqualsEqualsToken ? SyntaxKind.FalseKeyword : SyntaxKind.TrueKeyword));
34743+
if (isLeftNaN && isRightNaN) return;
34744+
const operatorString = operator === SyntaxKind.ExclamationEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken ? tokenToString(SyntaxKind.ExclamationToken) : "";
34745+
const location = isLeftNaN ? right : left;
34746+
const expression = skipParentheses(location);
34747+
addRelatedInfo(err, createDiagnosticForNode(location, Diagnostics.Did_you_mean_0,
34748+
`${operatorString}Number.isNaN(${isEntityNameExpression(expression) ? entityNameToString(expression) : "..."})`));
34749+
}
34750+
}
34751+
34752+
function isGlobalNaN(expr: Expression): boolean {
34753+
if (isIdentifier(expr) && expr.escapedText === "NaN") {
34754+
const globalNaNSymbol = getGlobalNaNSymbol();
34755+
return !!globalNaNSymbol && globalNaNSymbol === getResolvedSymbol(expr);
34756+
}
34757+
return false;
34758+
}
3473034759
}
3473134760

3473234761
function getBaseTypesIfUnrelated(leftType: Type, rightType: Type, isRelated: (left: Type, right: Type) => boolean): [Type, Type] {

src/compiler/diagnosticMessages.json

+12-1
Original file line numberDiff line numberDiff line change
@@ -3563,6 +3563,10 @@
35633563
"category": "Error",
35643564
"code": 2844
35653565
},
3566+
"This condition will always return '{0}'.": {
3567+
"category": "Error",
3568+
"code": 2845
3569+
},
35663570

35673571
"Import declaration '{0}' is using private name '{1}'.": {
35683572
"category": "Error",
@@ -7356,7 +7360,14 @@
73567360
"category": "Message",
73577361
"code": 95173
73587362
},
7359-
7363+
"Use `{0}`.": {
7364+
"category": "Message",
7365+
"code": 95174
7366+
},
7367+
"Use `Number.isNaN` in all conditions.": {
7368+
"category": "Message",
7369+
"code": 95175
7370+
},
73607371

73617372
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
73627373
"category": "Error",

src/services/codefixes/fixAddMissingConstraint.ts

-9
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,6 @@ namespace ts.codefix {
9494
}
9595
}
9696

97-
function findAncestorMatchingSpan(sourceFile: SourceFile, span: TextSpan): Node {
98-
const end = textSpanEnd(span);
99-
let token = getTokenAtPosition(sourceFile, span.start);
100-
while (token.end < end) {
101-
token = token.parent;
102-
}
103-
return token;
104-
}
105-
10697
function tryGetConstraintFromDiagnosticMessage(messageText: string | DiagnosticMessageChain) {
10798
const [_, constraint] = flattenDiagnosticMessageText(messageText, "\n", 0).match(/`extends (.*)`/) || [];
10899
return constraint;
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const fixId = "fixNaNEquality";
4+
const errorCodes = [
5+
Diagnostics.This_condition_will_always_return_0.code,
6+
];
7+
8+
registerCodeFix({
9+
errorCodes,
10+
getCodeActions(context) {
11+
const { sourceFile, span, program } = context;
12+
const info = getInfo(program, sourceFile, span);
13+
if (info === undefined) return;
14+
15+
const { suggestion, expression, arg } = info;
16+
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, arg, expression));
17+
return [createCodeFixAction(fixId, changes, [Diagnostics.Use_0, suggestion], fixId, Diagnostics.Use_Number_isNaN_in_all_conditions)];
18+
},
19+
fixIds: [fixId],
20+
getAllCodeActions: context => {
21+
return codeFixAll(context, errorCodes, (changes, diag) => {
22+
const info = getInfo(context.program, diag.file, createTextSpan(diag.start, diag.length));
23+
if (info) {
24+
doChange(changes, diag.file, info.arg, info.expression);
25+
}
26+
});
27+
}
28+
});
29+
30+
interface Info {
31+
suggestion: string;
32+
expression: BinaryExpression;
33+
arg: Expression;
34+
}
35+
36+
function getInfo(program: Program, sourceFile: SourceFile, span: TextSpan): Info | undefined {
37+
const diag = find(program.getSemanticDiagnostics(sourceFile), diag => diag.start === span.start && diag.length === span.length);
38+
if (diag === undefined || diag.relatedInformation === undefined) return;
39+
40+
const related = find(diag.relatedInformation, related => related.code === Diagnostics.Did_you_mean_0.code);
41+
if (related === undefined || related.file === undefined || related.start === undefined || related.length === undefined) return;
42+
43+
const token = findAncestorMatchingSpan(related.file, createTextSpan(related.start, related.length));
44+
if (token === undefined) return;
45+
46+
if (isExpression(token) && isBinaryExpression(token.parent)) {
47+
return { suggestion: getSuggestion(related.messageText), expression: token.parent, arg: token };
48+
}
49+
return undefined;
50+
}
51+
52+
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, arg: Expression, expression: BinaryExpression) {
53+
const callExpression = factory.createCallExpression(
54+
factory.createPropertyAccessExpression(factory.createIdentifier("Number"), factory.createIdentifier("isNaN")), /*typeArguments*/ undefined, [arg]);
55+
const operator = expression.operatorToken.kind ;
56+
changes.replaceNode(sourceFile, expression,
57+
operator === SyntaxKind.ExclamationEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken
58+
? factory.createPrefixUnaryExpression(SyntaxKind.ExclamationToken, callExpression) : callExpression);
59+
}
60+
61+
function getSuggestion(messageText: string | DiagnosticMessageChain) {
62+
const [_, suggestion] = flattenDiagnosticMessageText(messageText, "\n", 0).match(/\'(.*)\'/) || [];
63+
return suggestion;
64+
}
65+
}

src/services/codefixes/helpers.ts

+9
Original file line numberDiff line numberDiff line change
@@ -737,4 +737,13 @@ namespace ts.codefix {
737737
export function importSymbols(importAdder: ImportAdder, symbols: readonly Symbol[]) {
738738
symbols.forEach(s => importAdder.addImportFromExportedSymbol(s, /*isValidTypeOnlyUseSite*/ true));
739739
}
740+
741+
export function findAncestorMatchingSpan(sourceFile: SourceFile, span: TextSpan): Node {
742+
const end = textSpanEnd(span);
743+
let token = getTokenAtPosition(sourceFile, span.start);
744+
while (token.end < end) {
745+
token = token.parent;
746+
}
747+
return token;
748+
}
740749
}

src/services/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"codefixes/fixConstructorForDerivedNeedSuperCall.ts",
8383
"codefixes/fixEnableExperimentalDecorators.ts",
8484
"codefixes/fixEnableJsxFlag.ts",
85+
"codefixes/fixNaNEquality.ts",
8586
"codefixes/fixModuleAndTargetOptions.ts",
8687
"codefixes/fixPropertyAssignment.ts",
8788
"codefixes/fixExtendsInterfaceBecomesImplements.ts",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
tests/cases/compiler/nanEquality.ts(3,5): error TS2845: This condition will always return 'false'.
2+
tests/cases/compiler/nanEquality.ts(4,5): error TS2845: This condition will always return 'false'.
3+
tests/cases/compiler/nanEquality.ts(6,5): error TS2845: This condition will always return 'false'.
4+
tests/cases/compiler/nanEquality.ts(7,5): error TS2845: This condition will always return 'false'.
5+
tests/cases/compiler/nanEquality.ts(9,5): error TS2845: This condition will always return 'true'.
6+
tests/cases/compiler/nanEquality.ts(10,5): error TS2845: This condition will always return 'true'.
7+
tests/cases/compiler/nanEquality.ts(12,5): error TS2845: This condition will always return 'true'.
8+
tests/cases/compiler/nanEquality.ts(13,5): error TS2845: This condition will always return 'true'.
9+
tests/cases/compiler/nanEquality.ts(15,5): error TS2845: This condition will always return 'false'.
10+
tests/cases/compiler/nanEquality.ts(16,5): error TS2845: This condition will always return 'false'.
11+
tests/cases/compiler/nanEquality.ts(18,5): error TS2845: This condition will always return 'true'.
12+
tests/cases/compiler/nanEquality.ts(19,5): error TS2845: This condition will always return 'true'.
13+
tests/cases/compiler/nanEquality.ts(21,5): error TS2845: This condition will always return 'false'.
14+
tests/cases/compiler/nanEquality.ts(22,5): error TS2845: This condition will always return 'true'.
15+
tests/cases/compiler/nanEquality.ts(24,5): error TS2845: This condition will always return 'false'.
16+
tests/cases/compiler/nanEquality.ts(25,5): error TS2845: This condition will always return 'true'.
17+
tests/cases/compiler/nanEquality.ts(29,5): error TS2845: This condition will always return 'false'.
18+
19+
20+
==== tests/cases/compiler/nanEquality.ts (17 errors) ====
21+
declare const x: number;
22+
23+
if (x === NaN) {}
24+
~~~~~~~~~
25+
!!! error TS2845: This condition will always return 'false'.
26+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:3:5: Did you mean 'Number.isNaN(x)'?
27+
if (NaN === x) {}
28+
~~~~~~~~~
29+
!!! error TS2845: This condition will always return 'false'.
30+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:4:13: Did you mean 'Number.isNaN(x)'?
31+
32+
if (x == NaN) {}
33+
~~~~~~~~
34+
!!! error TS2845: This condition will always return 'false'.
35+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:6:5: Did you mean 'Number.isNaN(x)'?
36+
if (NaN == x) {}
37+
~~~~~~~~
38+
!!! error TS2845: This condition will always return 'false'.
39+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:7:12: Did you mean 'Number.isNaN(x)'?
40+
41+
if (x !== NaN) {}
42+
~~~~~~~~~
43+
!!! error TS2845: This condition will always return 'true'.
44+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:9:5: Did you mean '!Number.isNaN(x)'?
45+
if (NaN !== x) {}
46+
~~~~~~~~~
47+
!!! error TS2845: This condition will always return 'true'.
48+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:10:13: Did you mean '!Number.isNaN(x)'?
49+
50+
if (x != NaN) {}
51+
~~~~~~~~
52+
!!! error TS2845: This condition will always return 'true'.
53+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:12:5: Did you mean '!Number.isNaN(x)'?
54+
if (NaN != x) {}
55+
~~~~~~~~
56+
!!! error TS2845: This condition will always return 'true'.
57+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:13:12: Did you mean '!Number.isNaN(x)'?
58+
59+
if (x === ((NaN))) {}
60+
~~~~~~~~~~~~~
61+
!!! error TS2845: This condition will always return 'false'.
62+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:15:5: Did you mean 'Number.isNaN(x)'?
63+
if (((NaN)) === x) {}
64+
~~~~~~~~~~~~~
65+
!!! error TS2845: This condition will always return 'false'.
66+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:16:17: Did you mean 'Number.isNaN(x)'?
67+
68+
if (x !== ((NaN))) {}
69+
~~~~~~~~~~~~~
70+
!!! error TS2845: This condition will always return 'true'.
71+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:18:5: Did you mean '!Number.isNaN(x)'?
72+
if (((NaN)) !== x) {}
73+
~~~~~~~~~~~~~
74+
!!! error TS2845: This condition will always return 'true'.
75+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:19:17: Did you mean '!Number.isNaN(x)'?
76+
77+
if (NaN === NaN) {}
78+
~~~~~~~~~~~
79+
!!! error TS2845: This condition will always return 'false'.
80+
if (NaN !== NaN) {}
81+
~~~~~~~~~~~
82+
!!! error TS2845: This condition will always return 'true'.
83+
84+
if (NaN == NaN) {}
85+
~~~~~~~~~~
86+
!!! error TS2845: This condition will always return 'false'.
87+
if (NaN != NaN) {}
88+
~~~~~~~~~~
89+
!!! error TS2845: This condition will always return 'true'.
90+
91+
// ...
92+
declare let y: any;
93+
if (NaN === y[0][1]) {}
94+
~~~~~~~~~~~~~~~
95+
!!! error TS2845: This condition will always return 'false'.
96+
!!! related TS1369 tests/cases/compiler/nanEquality.ts:29:13: Did you mean 'Number.isNaN(...)'?
97+
98+
function t1(value: number, NaN: number) {
99+
return value === NaN; // ok
100+
}
101+
102+
function t2(value: number, NaN: number) {
103+
return NaN == value; // ok
104+
}
105+
106+
function t3(NaN: number) {
107+
return NaN === NaN; // ok
108+
}
109+
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//// [nanEquality.ts]
2+
declare const x: number;
3+
4+
if (x === NaN) {}
5+
if (NaN === x) {}
6+
7+
if (x == NaN) {}
8+
if (NaN == x) {}
9+
10+
if (x !== NaN) {}
11+
if (NaN !== x) {}
12+
13+
if (x != NaN) {}
14+
if (NaN != x) {}
15+
16+
if (x === ((NaN))) {}
17+
if (((NaN)) === x) {}
18+
19+
if (x !== ((NaN))) {}
20+
if (((NaN)) !== x) {}
21+
22+
if (NaN === NaN) {}
23+
if (NaN !== NaN) {}
24+
25+
if (NaN == NaN) {}
26+
if (NaN != NaN) {}
27+
28+
// ...
29+
declare let y: any;
30+
if (NaN === y[0][1]) {}
31+
32+
function t1(value: number, NaN: number) {
33+
return value === NaN; // ok
34+
}
35+
36+
function t2(value: number, NaN: number) {
37+
return NaN == value; // ok
38+
}
39+
40+
function t3(NaN: number) {
41+
return NaN === NaN; // ok
42+
}
43+
44+
45+
//// [nanEquality.js]
46+
if (x === NaN) { }
47+
if (NaN === x) { }
48+
if (x == NaN) { }
49+
if (NaN == x) { }
50+
if (x !== NaN) { }
51+
if (NaN !== x) { }
52+
if (x != NaN) { }
53+
if (NaN != x) { }
54+
if (x === ((NaN))) { }
55+
if (((NaN)) === x) { }
56+
if (x !== ((NaN))) { }
57+
if (((NaN)) !== x) { }
58+
if (NaN === NaN) { }
59+
if (NaN !== NaN) { }
60+
if (NaN == NaN) { }
61+
if (NaN != NaN) { }
62+
if (NaN === y[0][1]) { }
63+
function t1(value, NaN) {
64+
return value === NaN; // ok
65+
}
66+
function t2(value, NaN) {
67+
return NaN == value; // ok
68+
}
69+
function t3(NaN) {
70+
return NaN === NaN; // ok
71+
}

0 commit comments

Comments
 (0)