refactor(tsetse): optimize equals-nan rule

`getText()` should be avoid as it as an expensive call; favor node 'isXYZ' checks and direct `text` property access.

simplify rule registration method overloads

uses type property directly to remove the need for specialized overloads for each TS node type

cleanup ban-expect-truthy-promise rule

Closes #346

PiperOrigin-RevId: 225020913
diff --git a/internal/tsetse/checker.ts b/internal/tsetse/checker.ts
index 5e8b6d3..72fdbaa 100644
--- a/internal/tsetse/checker.ts
+++ b/internal/tsetse/checker.ts
@@ -47,36 +47,8 @@
    * `nodeKind` node in `nodeHandlersMap` map. After all rules register their
    * handlers, the source file AST will be traversed.
    */
-  on(nodeKind: ts.SyntaxKind.BinaryExpression,
-     handlerFunction: (checker: Checker, node: ts.BinaryExpression) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind.ConditionalExpression,
-     handlerFunction:
-         (checker: Checker, node: ts.ConditionalExpression) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind.WhileStatement,
-     handlerFunction: (checker: Checker, node: ts.WhileStatement) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind.IfStatement,
-     handlerFunction: (checker: Checker, node: ts.IfStatement) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind.CallExpression,
-     handlerFunction: (checker: Checker, node: ts.CallExpression) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind.PropertyDeclaration,
-     handlerFunction: (checker: Checker, node: ts.PropertyDeclaration) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind.ElementAccessExpression,
-     handlerFunction: (checker: Checker, node: ts.ElementAccessExpression) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind.ImportDeclaration,
-     handlerFunction: (checker: Checker, node: ts.ImportDeclaration) => void,
-     code: number): void;
-  on(nodeKind: ts.SyntaxKind,
-     handlerFunction: (checker: Checker, node: ts.Node) => void,
-     code: number): void;
   on<T extends ts.Node>(
-      nodeKind: ts.SyntaxKind,
+      nodeKind: T['kind'],
       handlerFunction: (checker: Checker, node: T) => void, code: number) {
     const newHandler: Handler = {handlerFunction, code};
     const registeredHandlers: Handler[]|undefined =
diff --git a/internal/tsetse/rules/ban_expect_truthy_promise_rule.ts b/internal/tsetse/rules/ban_expect_truthy_promise_rule.ts
index 6f852ae..fbbefd9 100644
--- a/internal/tsetse/rules/ban_expect_truthy_promise_rule.ts
+++ b/internal/tsetse/rules/ban_expect_truthy_promise_rule.ts
@@ -21,23 +21,27 @@
   }
 }
 
-function checkForTruthy(checker: Checker, node: ts.Node) {
-  const tc = checker.typeChecker;
-
-  if (!tsutils.isPropertyAccessExpression(node)) {
+function checkForTruthy(checker: Checker, node: ts.PropertyAccessExpression) {
+  if (node.name.text !== 'toBeTruthy') {
     return;
   }
 
-  if (node.name.getText() !== 'toBeTruthy') {
-    return;
-  }
-
-  const expectCallNode = getLeftmostNode(tc, node);
+  const expectCallNode = getLeftmostNode(node);
   if (!ts.isCallExpression(expectCallNode)) {
     return;
   }
 
-  const signature = checker.typeChecker.getResolvedSignature(expectCallNode);
+  if (!ts.isIdentifier(expectCallNode.expression) || expectCallNode.expression.text !== 'expect') {
+    return;
+  }
+
+  if (expectCallNode.arguments.length === 0 || ts.isAwaitExpression(expectCallNode.arguments[0])) {
+    return;
+  }
+
+  const tc = checker.typeChecker;
+
+  const signature = tc.getResolvedSignature(expectCallNode);
   if (signature === undefined) {
     return;
   }
@@ -48,13 +52,11 @@
   }
 
   // Only look for methods named expect that return a Matchers
-  if (!((symbol.name === 'Matchers') &&
-        expectCallNode.expression.getText() === 'expect')) {
+  if (symbol.name !== 'Matchers') {
     return;
   }
 
-  if (!tsutils.isThenableType(tc, expectCallNode.arguments[0]) ||
-      tsutils.isAwaitExpression(expectCallNode.arguments[0])) {
+  if (!tsutils.isThenableType(tc, expectCallNode.arguments[0])) {
     return;
   }
 
@@ -67,8 +69,7 @@
           `\n\tSee http://tsetse.info/ban-expect-truthy-promise`);
 }
 
-function getLeftmostNode(
-    tc: ts.TypeChecker, node: ts.PropertyAccessExpression) {
+function getLeftmostNode(node: ts.PropertyAccessExpression) {
   let current: ts.LeftHandSideExpression|undefined = node;
   while (ts.isPropertyAccessExpression(current)) {
     current = current.expression;
diff --git a/internal/tsetse/rules/equals_nan_rule.ts b/internal/tsetse/rules/equals_nan_rule.ts
index da8f7fe..38b01d5 100644
--- a/internal/tsetse/rules/equals_nan_rule.ts
+++ b/internal/tsetse/rules/equals_nan_rule.ts
@@ -20,19 +20,41 @@
 }
 
 function checkBinaryExpression(checker: Checker, node: ts.BinaryExpression) {
-  if (node.left.getText() === 'NaN' || node.right.getText() === 'NaN') {
-    const operator = node.operatorToken;
-    if (operator.kind == ts.SyntaxKind.EqualsEqualsToken ||
-        operator.kind === ts.SyntaxKind.EqualsEqualsEqualsToken) {
+  const isLeftNaN = ts.isIdentifier(node.left) && node.left.text === 'NaN';
+  const isRightNaN = ts.isIdentifier(node.right) && node.right.text === 'NaN';
+  if (!isLeftNaN && !isRightNaN) {
+    return;
+  }
+
+  // We avoid calling getText() on the node.operatorToken because it's slow.
+  // Instead, manually map back from the kind to the string form of the operator
+  switch (node.operatorToken.kind) {
+    case ts.SyntaxKind.EqualsEqualsToken:
       checker.addFailureAtNode(
-          node,
-          `x ${operator.getText()} NaN is always false; use isNaN(x) instead`);
-    }
-    if (operator.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
-        operator.kind === ts.SyntaxKind.ExclamationEqualsToken) {
+        node,
+        `x == NaN is always false; use isNaN(x) instead`,
+      );
+      break;
+    case ts.SyntaxKind.EqualsEqualsEqualsToken:
       checker.addFailureAtNode(
-          node,
-          `x ${operator.getText()} NaN is always true; use !isNaN(x) instead`);
-    }
+        node,
+        `x === NaN is always false; use isNaN(x) instead`,
+      );
+      break;
+    case ts.SyntaxKind.ExclamationEqualsToken:
+      checker.addFailureAtNode(
+        node,
+        `x != NaN is always true; use !isNaN(x) instead`,
+      );
+      break;
+    case ts.SyntaxKind.ExclamationEqualsEqualsToken:
+      checker.addFailureAtNode(
+        node,
+        `x !== NaN is always true; use !isNaN(x) instead`,
+      );
+      break;
+    default:
+      // We don't care about other operators acting on NaN
+      break;
   }
 }