From e002159ad133a024bae48a2e190e54ad93f6b52d Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Tue, 20 Sep 2022 23:16:44 +0300 Subject: [PATCH] feat(49962): Disallow comparison against NaN (#50626) * feat(49962): disallow comparison against NaN * change diagnostic message * use global NaN symbol for NaN equality comparisons --- src/compiler/checker.ts | 29 ++++ src/compiler/diagnosticMessages.json | 13 +- .../codefixes/fixAddMissingConstraint.ts | 9 -- src/services/codefixes/fixNaNEquality.ts | 65 +++++++++ src/services/codefixes/helpers.ts | 9 ++ src/services/tsconfig.json | 1 + .../reference/nanEquality.errors.txt | 109 ++++++++++++++ tests/baselines/reference/nanEquality.js | 71 +++++++++ tests/baselines/reference/nanEquality.symbols | 105 ++++++++++++++ tests/baselines/reference/nanEquality.types | 137 ++++++++++++++++++ tests/cases/compiler/nanEquality.ts | 41 ++++++ tests/cases/fourslash/fixNaNEquality1.ts | 10 ++ tests/cases/fourslash/fixNaNEquality2.ts | 10 ++ tests/cases/fourslash/fixNaNEquality3.ts | 10 ++ tests/cases/fourslash/fixNaNEquality4.ts | 10 ++ tests/cases/fourslash/fixNaNEquality5.ts | 10 ++ tests/cases/fourslash/fixNaNEquality_all.ts | 22 +++ 17 files changed, 651 insertions(+), 10 deletions(-) create mode 100644 src/services/codefixes/fixNaNEquality.ts create mode 100644 tests/baselines/reference/nanEquality.errors.txt create mode 100644 tests/baselines/reference/nanEquality.js create mode 100644 tests/baselines/reference/nanEquality.symbols create mode 100644 tests/baselines/reference/nanEquality.types create mode 100644 tests/cases/compiler/nanEquality.ts create mode 100644 tests/cases/fourslash/fixNaNEquality1.ts create mode 100644 tests/cases/fourslash/fixNaNEquality2.ts create mode 100644 tests/cases/fourslash/fixNaNEquality3.ts create mode 100644 tests/cases/fourslash/fixNaNEquality4.ts create mode 100644 tests/cases/fourslash/fixNaNEquality5.ts create mode 100644 tests/cases/fourslash/fixNaNEquality_all.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 96cd22e9c7aa8..941803ea465b1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -999,6 +999,7 @@ namespace ts { let deferredGlobalOmitSymbol: Symbol | undefined; let deferredGlobalAwaitedSymbol: Symbol | undefined; let deferredGlobalBigIntType: ObjectType | undefined; + let deferredGlobalNaNSymbol: Symbol | undefined; let deferredGlobalRecordSymbol: Symbol | undefined; const allPotentiallyUnusedIdentifiers = new Map(); // key is file name @@ -14343,6 +14344,10 @@ namespace ts { return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as __String, /*arity*/ 0, /*reportErrors*/ false)) || emptyObjectType; } + function getGlobalNaNSymbol(): Symbol | undefined { + return (deferredGlobalNaNSymbol ||= getGlobalValueSymbol("NaN" as __String, /*reportErrors*/ false)); + } + function getGlobalRecordSymbol(): Symbol | undefined { deferredGlobalRecordSymbol ||= getGlobalTypeAliasSymbol("Record" as __String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol; return deferredGlobalRecordSymbol === unknownSymbol ? undefined : deferredGlobalRecordSymbol; @@ -34495,6 +34500,7 @@ namespace ts { const eqType = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken; error(errorNode, Diagnostics.This_condition_will_always_return_0_since_JavaScript_compares_objects_by_reference_not_value, eqType ? "false" : "true"); } + checkNaNEquality(errorNode, operator, left, right); reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); return booleanType; @@ -34727,6 +34733,29 @@ namespace ts { return undefined; } } + + function checkNaNEquality(errorNode: Node | undefined, operator: SyntaxKind, left: Expression, right: Expression) { + const isLeftNaN = isGlobalNaN(skipParentheses(left)); + const isRightNaN = isGlobalNaN(skipParentheses(right)); + if (isLeftNaN || isRightNaN) { + const err = error(errorNode, Diagnostics.This_condition_will_always_return_0, + tokenToString(operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.EqualsEqualsToken ? SyntaxKind.FalseKeyword : SyntaxKind.TrueKeyword)); + if (isLeftNaN && isRightNaN) return; + const operatorString = operator === SyntaxKind.ExclamationEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken ? tokenToString(SyntaxKind.ExclamationToken) : ""; + const location = isLeftNaN ? right : left; + const expression = skipParentheses(location); + addRelatedInfo(err, createDiagnosticForNode(location, Diagnostics.Did_you_mean_0, + `${operatorString}Number.isNaN(${isEntityNameExpression(expression) ? entityNameToString(expression) : "..."})`)); + } + } + + function isGlobalNaN(expr: Expression): boolean { + if (isIdentifier(expr) && expr.escapedText === "NaN") { + const globalNaNSymbol = getGlobalNaNSymbol(); + return !!globalNaNSymbol && globalNaNSymbol === getResolvedSymbol(expr); + } + return false; + } } function getBaseTypesIfUnrelated(leftType: Type, rightType: Type, isRelated: (left: Type, right: Type) => boolean): [Type, Type] { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index ad6b6615fcab6..7a8f3192d917c 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3563,6 +3563,10 @@ "category": "Error", "code": 2844 }, + "This condition will always return '{0}'.": { + "category": "Error", + "code": 2845 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", @@ -7356,7 +7360,14 @@ "category": "Message", "code": 95173 }, - + "Use `{0}`.": { + "category": "Message", + "code": 95174 + }, + "Use `Number.isNaN` in all conditions.": { + "category": "Message", + "code": 95175 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/services/codefixes/fixAddMissingConstraint.ts b/src/services/codefixes/fixAddMissingConstraint.ts index 2d3b6c51a346c..c8cbce9ee02af 100644 --- a/src/services/codefixes/fixAddMissingConstraint.ts +++ b/src/services/codefixes/fixAddMissingConstraint.ts @@ -94,15 +94,6 @@ namespace ts.codefix { } } - function findAncestorMatchingSpan(sourceFile: SourceFile, span: TextSpan): Node { - const end = textSpanEnd(span); - let token = getTokenAtPosition(sourceFile, span.start); - while (token.end < end) { - token = token.parent; - } - return token; - } - function tryGetConstraintFromDiagnosticMessage(messageText: string | DiagnosticMessageChain) { const [_, constraint] = flattenDiagnosticMessageText(messageText, "\n", 0).match(/`extends (.*)`/) || []; return constraint; diff --git a/src/services/codefixes/fixNaNEquality.ts b/src/services/codefixes/fixNaNEquality.ts new file mode 100644 index 0000000000000..7448aa52be725 --- /dev/null +++ b/src/services/codefixes/fixNaNEquality.ts @@ -0,0 +1,65 @@ +/* @internal */ +namespace ts.codefix { + const fixId = "fixNaNEquality"; + const errorCodes = [ + Diagnostics.This_condition_will_always_return_0.code, + ]; + + registerCodeFix({ + errorCodes, + getCodeActions(context) { + const { sourceFile, span, program } = context; + const info = getInfo(program, sourceFile, span); + if (info === undefined) return; + + const { suggestion, expression, arg } = info; + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, arg, expression)); + return [createCodeFixAction(fixId, changes, [Diagnostics.Use_0, suggestion], fixId, Diagnostics.Use_Number_isNaN_in_all_conditions)]; + }, + fixIds: [fixId], + getAllCodeActions: context => { + return codeFixAll(context, errorCodes, (changes, diag) => { + const info = getInfo(context.program, diag.file, createTextSpan(diag.start, diag.length)); + if (info) { + doChange(changes, diag.file, info.arg, info.expression); + } + }); + } + }); + + interface Info { + suggestion: string; + expression: BinaryExpression; + arg: Expression; + } + + function getInfo(program: Program, sourceFile: SourceFile, span: TextSpan): Info | undefined { + const diag = find(program.getSemanticDiagnostics(sourceFile), diag => diag.start === span.start && diag.length === span.length); + if (diag === undefined || diag.relatedInformation === undefined) return; + + const related = find(diag.relatedInformation, related => related.code === Diagnostics.Did_you_mean_0.code); + if (related === undefined || related.file === undefined || related.start === undefined || related.length === undefined) return; + + const token = findAncestorMatchingSpan(related.file, createTextSpan(related.start, related.length)); + if (token === undefined) return; + + if (isExpression(token) && isBinaryExpression(token.parent)) { + return { suggestion: getSuggestion(related.messageText), expression: token.parent, arg: token }; + } + return undefined; + } + + function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, arg: Expression, expression: BinaryExpression) { + const callExpression = factory.createCallExpression( + factory.createPropertyAccessExpression(factory.createIdentifier("Number"), factory.createIdentifier("isNaN")), /*typeArguments*/ undefined, [arg]); + const operator = expression.operatorToken.kind ; + changes.replaceNode(sourceFile, expression, + operator === SyntaxKind.ExclamationEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken + ? factory.createPrefixUnaryExpression(SyntaxKind.ExclamationToken, callExpression) : callExpression); + } + + function getSuggestion(messageText: string | DiagnosticMessageChain) { + const [_, suggestion] = flattenDiagnosticMessageText(messageText, "\n", 0).match(/\'(.*)\'/) || []; + return suggestion; + } +} diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 6353834280970..f125660ef5edd 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -737,4 +737,13 @@ namespace ts.codefix { export function importSymbols(importAdder: ImportAdder, symbols: readonly Symbol[]) { symbols.forEach(s => importAdder.addImportFromExportedSymbol(s, /*isValidTypeOnlyUseSite*/ true)); } + + export function findAncestorMatchingSpan(sourceFile: SourceFile, span: TextSpan): Node { + const end = textSpanEnd(span); + let token = getTokenAtPosition(sourceFile, span.start); + while (token.end < end) { + token = token.parent; + } + return token; + } } diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 949d5c8a964a7..2237163ebb20d 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -82,6 +82,7 @@ "codefixes/fixConstructorForDerivedNeedSuperCall.ts", "codefixes/fixEnableExperimentalDecorators.ts", "codefixes/fixEnableJsxFlag.ts", + "codefixes/fixNaNEquality.ts", "codefixes/fixModuleAndTargetOptions.ts", "codefixes/fixPropertyAssignment.ts", "codefixes/fixExtendsInterfaceBecomesImplements.ts", diff --git a/tests/baselines/reference/nanEquality.errors.txt b/tests/baselines/reference/nanEquality.errors.txt new file mode 100644 index 0000000000000..410d782d4b16c --- /dev/null +++ b/tests/baselines/reference/nanEquality.errors.txt @@ -0,0 +1,109 @@ +tests/cases/compiler/nanEquality.ts(3,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(4,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(6,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(7,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(9,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(10,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(12,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(13,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(15,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(16,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(18,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(19,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(21,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(22,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(24,5): error TS2845: This condition will always return 'false'. +tests/cases/compiler/nanEquality.ts(25,5): error TS2845: This condition will always return 'true'. +tests/cases/compiler/nanEquality.ts(29,5): error TS2845: This condition will always return 'false'. + + +==== tests/cases/compiler/nanEquality.ts (17 errors) ==== + declare const x: number; + + if (x === NaN) {} + ~~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:3:5: Did you mean 'Number.isNaN(x)'? + if (NaN === x) {} + ~~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:4:13: Did you mean 'Number.isNaN(x)'? + + if (x == NaN) {} + ~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:6:5: Did you mean 'Number.isNaN(x)'? + if (NaN == x) {} + ~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:7:12: Did you mean 'Number.isNaN(x)'? + + if (x !== NaN) {} + ~~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:9:5: Did you mean '!Number.isNaN(x)'? + if (NaN !== x) {} + ~~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:10:13: Did you mean '!Number.isNaN(x)'? + + if (x != NaN) {} + ~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:12:5: Did you mean '!Number.isNaN(x)'? + if (NaN != x) {} + ~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:13:12: Did you mean '!Number.isNaN(x)'? + + if (x === ((NaN))) {} + ~~~~~~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:15:5: Did you mean 'Number.isNaN(x)'? + if (((NaN)) === x) {} + ~~~~~~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:16:17: Did you mean 'Number.isNaN(x)'? + + if (x !== ((NaN))) {} + ~~~~~~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:18:5: Did you mean '!Number.isNaN(x)'? + if (((NaN)) !== x) {} + ~~~~~~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:19:17: Did you mean '!Number.isNaN(x)'? + + if (NaN === NaN) {} + ~~~~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. + if (NaN !== NaN) {} + ~~~~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. + + if (NaN == NaN) {} + ~~~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. + if (NaN != NaN) {} + ~~~~~~~~~~ +!!! error TS2845: This condition will always return 'true'. + + // ... + declare let y: any; + if (NaN === y[0][1]) {} + ~~~~~~~~~~~~~~~ +!!! error TS2845: This condition will always return 'false'. +!!! related TS1369 tests/cases/compiler/nanEquality.ts:29:13: Did you mean 'Number.isNaN(...)'? + + function t1(value: number, NaN: number) { + return value === NaN; // ok + } + + function t2(value: number, NaN: number) { + return NaN == value; // ok + } + + function t3(NaN: number) { + return NaN === NaN; // ok + } + \ No newline at end of file diff --git a/tests/baselines/reference/nanEquality.js b/tests/baselines/reference/nanEquality.js new file mode 100644 index 0000000000000..21a10e7d0fb91 --- /dev/null +++ b/tests/baselines/reference/nanEquality.js @@ -0,0 +1,71 @@ +//// [nanEquality.ts] +declare const x: number; + +if (x === NaN) {} +if (NaN === x) {} + +if (x == NaN) {} +if (NaN == x) {} + +if (x !== NaN) {} +if (NaN !== x) {} + +if (x != NaN) {} +if (NaN != x) {} + +if (x === ((NaN))) {} +if (((NaN)) === x) {} + +if (x !== ((NaN))) {} +if (((NaN)) !== x) {} + +if (NaN === NaN) {} +if (NaN !== NaN) {} + +if (NaN == NaN) {} +if (NaN != NaN) {} + +// ... +declare let y: any; +if (NaN === y[0][1]) {} + +function t1(value: number, NaN: number) { + return value === NaN; // ok +} + +function t2(value: number, NaN: number) { + return NaN == value; // ok +} + +function t3(NaN: number) { + return NaN === NaN; // ok +} + + +//// [nanEquality.js] +if (x === NaN) { } +if (NaN === x) { } +if (x == NaN) { } +if (NaN == x) { } +if (x !== NaN) { } +if (NaN !== x) { } +if (x != NaN) { } +if (NaN != x) { } +if (x === ((NaN))) { } +if (((NaN)) === x) { } +if (x !== ((NaN))) { } +if (((NaN)) !== x) { } +if (NaN === NaN) { } +if (NaN !== NaN) { } +if (NaN == NaN) { } +if (NaN != NaN) { } +if (NaN === y[0][1]) { } +function t1(value, NaN) { + return value === NaN; // ok +} +function t2(value, NaN) { + return NaN == value; // ok +} +function t3(NaN) { + return NaN === NaN; // ok +} diff --git a/tests/baselines/reference/nanEquality.symbols b/tests/baselines/reference/nanEquality.symbols new file mode 100644 index 0000000000000..4398b9c32f649 --- /dev/null +++ b/tests/baselines/reference/nanEquality.symbols @@ -0,0 +1,105 @@ +=== tests/cases/compiler/nanEquality.ts === +declare const x: number; +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) + +if (x === NaN) {} +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (NaN === x) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) + +if (x == NaN) {} +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (NaN == x) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) + +if (x !== NaN) {} +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (NaN !== x) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) + +if (x != NaN) {} +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (NaN != x) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) + +if (x === ((NaN))) {} +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (((NaN)) === x) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) + +if (x !== ((NaN))) {} +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (((NaN)) !== x) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>x : Symbol(x, Decl(nanEquality.ts, 0, 13)) + +if (NaN === NaN) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (NaN !== NaN) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (NaN == NaN) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +if (NaN != NaN) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) + +// ... +declare let y: any; +>y : Symbol(y, Decl(nanEquality.ts, 27, 11)) + +if (NaN === y[0][1]) {} +>NaN : Symbol(NaN, Decl(lib.es5.d.ts, --, --)) +>y : Symbol(y, Decl(nanEquality.ts, 27, 11)) + +function t1(value: number, NaN: number) { +>t1 : Symbol(t1, Decl(nanEquality.ts, 28, 23)) +>value : Symbol(value, Decl(nanEquality.ts, 30, 12)) +>NaN : Symbol(NaN, Decl(nanEquality.ts, 30, 26)) + + return value === NaN; // ok +>value : Symbol(value, Decl(nanEquality.ts, 30, 12)) +>NaN : Symbol(NaN, Decl(nanEquality.ts, 30, 26)) +} + +function t2(value: number, NaN: number) { +>t2 : Symbol(t2, Decl(nanEquality.ts, 32, 1)) +>value : Symbol(value, Decl(nanEquality.ts, 34, 12)) +>NaN : Symbol(NaN, Decl(nanEquality.ts, 34, 26)) + + return NaN == value; // ok +>NaN : Symbol(NaN, Decl(nanEquality.ts, 34, 26)) +>value : Symbol(value, Decl(nanEquality.ts, 34, 12)) +} + +function t3(NaN: number) { +>t3 : Symbol(t3, Decl(nanEquality.ts, 36, 1)) +>NaN : Symbol(NaN, Decl(nanEquality.ts, 38, 12)) + + return NaN === NaN; // ok +>NaN : Symbol(NaN, Decl(nanEquality.ts, 38, 12)) +>NaN : Symbol(NaN, Decl(nanEquality.ts, 38, 12)) +} + diff --git a/tests/baselines/reference/nanEquality.types b/tests/baselines/reference/nanEquality.types new file mode 100644 index 0000000000000..ce64d93017405 --- /dev/null +++ b/tests/baselines/reference/nanEquality.types @@ -0,0 +1,137 @@ +=== tests/cases/compiler/nanEquality.ts === +declare const x: number; +>x : number + +if (x === NaN) {} +>x === NaN : boolean +>x : number +>NaN : number + +if (NaN === x) {} +>NaN === x : boolean +>NaN : number +>x : number + +if (x == NaN) {} +>x == NaN : boolean +>x : number +>NaN : number + +if (NaN == x) {} +>NaN == x : boolean +>NaN : number +>x : number + +if (x !== NaN) {} +>x !== NaN : boolean +>x : number +>NaN : number + +if (NaN !== x) {} +>NaN !== x : boolean +>NaN : number +>x : number + +if (x != NaN) {} +>x != NaN : boolean +>x : number +>NaN : number + +if (NaN != x) {} +>NaN != x : boolean +>NaN : number +>x : number + +if (x === ((NaN))) {} +>x === ((NaN)) : boolean +>x : number +>((NaN)) : number +>(NaN) : number +>NaN : number + +if (((NaN)) === x) {} +>((NaN)) === x : boolean +>((NaN)) : number +>(NaN) : number +>NaN : number +>x : number + +if (x !== ((NaN))) {} +>x !== ((NaN)) : boolean +>x : number +>((NaN)) : number +>(NaN) : number +>NaN : number + +if (((NaN)) !== x) {} +>((NaN)) !== x : boolean +>((NaN)) : number +>(NaN) : number +>NaN : number +>x : number + +if (NaN === NaN) {} +>NaN === NaN : boolean +>NaN : number +>NaN : number + +if (NaN !== NaN) {} +>NaN !== NaN : boolean +>NaN : number +>NaN : number + +if (NaN == NaN) {} +>NaN == NaN : boolean +>NaN : number +>NaN : number + +if (NaN != NaN) {} +>NaN != NaN : boolean +>NaN : number +>NaN : number + +// ... +declare let y: any; +>y : any + +if (NaN === y[0][1]) {} +>NaN === y[0][1] : boolean +>NaN : number +>y[0][1] : any +>y[0] : any +>y : any +>0 : 0 +>1 : 1 + +function t1(value: number, NaN: number) { +>t1 : (value: number, NaN: number) => boolean +>value : number +>NaN : number + + return value === NaN; // ok +>value === NaN : boolean +>value : number +>NaN : number +} + +function t2(value: number, NaN: number) { +>t2 : (value: number, NaN: number) => boolean +>value : number +>NaN : number + + return NaN == value; // ok +>NaN == value : boolean +>NaN : number +>value : number +} + +function t3(NaN: number) { +>t3 : (NaN: number) => boolean +>NaN : number + + return NaN === NaN; // ok +>NaN === NaN : boolean +>NaN : number +>NaN : number +} + diff --git a/tests/cases/compiler/nanEquality.ts b/tests/cases/compiler/nanEquality.ts new file mode 100644 index 0000000000000..88d949c8eaa9d --- /dev/null +++ b/tests/cases/compiler/nanEquality.ts @@ -0,0 +1,41 @@ +declare const x: number; + +if (x === NaN) {} +if (NaN === x) {} + +if (x == NaN) {} +if (NaN == x) {} + +if (x !== NaN) {} +if (NaN !== x) {} + +if (x != NaN) {} +if (NaN != x) {} + +if (x === ((NaN))) {} +if (((NaN)) === x) {} + +if (x !== ((NaN))) {} +if (((NaN)) !== x) {} + +if (NaN === NaN) {} +if (NaN !== NaN) {} + +if (NaN == NaN) {} +if (NaN != NaN) {} + +// ... +declare let y: any; +if (NaN === y[0][1]) {} + +function t1(value: number, NaN: number) { + return value === NaN; // ok +} + +function t2(value: number, NaN: number) { + return NaN == value; // ok +} + +function t3(NaN: number) { + return NaN === NaN; // ok +} diff --git a/tests/cases/fourslash/fixNaNEquality1.ts b/tests/cases/fourslash/fixNaNEquality1.ts new file mode 100644 index 0000000000000..52004b122fa2d --- /dev/null +++ b/tests/cases/fourslash/fixNaNEquality1.ts @@ -0,0 +1,10 @@ +/// + +////declare const x: number; +////[|if (x === NaN) {}|] + +verify.codeFix({ + index: 0, + description: "Use `Number.isNaN(x)`.", + newRangeContent: "if (Number.isNaN(x)) {}", +}); diff --git a/tests/cases/fourslash/fixNaNEquality2.ts b/tests/cases/fourslash/fixNaNEquality2.ts new file mode 100644 index 0000000000000..e941ac7bd4f61 --- /dev/null +++ b/tests/cases/fourslash/fixNaNEquality2.ts @@ -0,0 +1,10 @@ +/// + +////declare const x: number; +////[|if (NaN === x) {}|] + +verify.codeFix({ + index: 0, + description: "Use `Number.isNaN(x)`.", + newRangeContent: "if (Number.isNaN(x)) {}", +}); diff --git a/tests/cases/fourslash/fixNaNEquality3.ts b/tests/cases/fourslash/fixNaNEquality3.ts new file mode 100644 index 0000000000000..6ae682a83c9a5 --- /dev/null +++ b/tests/cases/fourslash/fixNaNEquality3.ts @@ -0,0 +1,10 @@ +/// + +////declare const x: number; +////[|if (x !== NaN) {}|] + +verify.codeFix({ + index: 0, + description: "Use `!Number.isNaN(x)`.", + newRangeContent: "if (!Number.isNaN(x)) {}", +}); diff --git a/tests/cases/fourslash/fixNaNEquality4.ts b/tests/cases/fourslash/fixNaNEquality4.ts new file mode 100644 index 0000000000000..6f4128317b977 --- /dev/null +++ b/tests/cases/fourslash/fixNaNEquality4.ts @@ -0,0 +1,10 @@ +/// + +////declare const x: number; +////[|if (NaN !== x) {}|] + +verify.codeFix({ + index: 0, + description: "Use `!Number.isNaN(x)`.", + newRangeContent: "if (!Number.isNaN(x)) {}", +}); diff --git a/tests/cases/fourslash/fixNaNEquality5.ts b/tests/cases/fourslash/fixNaNEquality5.ts new file mode 100644 index 0000000000000..00b954f33bd59 --- /dev/null +++ b/tests/cases/fourslash/fixNaNEquality5.ts @@ -0,0 +1,10 @@ +/// + +////declare const x: any; +////[|if (NaN !== x[0][1]) {}|] + +verify.codeFix({ + index: 0, + description: "Use `!Number.isNaN(...)`.", + newRangeContent: "if (!Number.isNaN(x[0][1])) {}", +}); diff --git a/tests/cases/fourslash/fixNaNEquality_all.ts b/tests/cases/fourslash/fixNaNEquality_all.ts new file mode 100644 index 0000000000000..e5536ec8f041b --- /dev/null +++ b/tests/cases/fourslash/fixNaNEquality_all.ts @@ -0,0 +1,22 @@ +/// + +////declare const x: number; +////declare const y: any; +////if (x === NaN) {} +////if (NaN === x) {} +////if (x !== NaN) {} +////if (NaN !== x) {} +////if (NaN === y[0][1]) {} + +verify.codeFixAll({ + fixId: "fixNaNEquality", + fixAllDescription: ts.Diagnostics.Use_Number_isNaN_in_all_conditions.message, + newFileContent: +`declare const x: number; +declare const y: any; +if (Number.isNaN(x)) {} +if (Number.isNaN(x)) {} +if (!Number.isNaN(x)) {} +if (!Number.isNaN(x)) {} +if (Number.isNaN(y[0][1])) {}` +});