Skip to content

require-invalid-date-check-before-compare: isNaN(getTime()) validation ignores position/reachability relative to the comparison #51525

Description

@github-actions

Rule

require-invalid-date-check-before-compare (eslint-factory/src/rules/require-invalid-date-check-before-compare.ts) — first review since it shipped; no prior issues filed.

What the rule does

Flags a relational comparison (<, >, <=, >=) involving a new Date(nonTrivialArg) value unless that value was validated via Number.isNaN(x.getTime()) / isNaN(x.getTime()) somewhere in the file. Tracking is scope-aware (uses resolved Variable objects, so same-named locals in different function scopes are correctly kept separate — verified by the existing test "same variable name declared in a different function scope is not treated as validated").

The gap

The validated Set<Variable> is populated purely by whether a Number.isNaN(x.getTime()) call for that variable exists anywhere in the traversal, with no check that the guard is reachable and executes before the risky comparison. Concretely, in create():

  • CallExpression visitor adds a variable to validated unconditionally whenever isGetTimeNaNCheck matches, regardless of source position.
  • BinaryExpression visitor unconditionally records every relational comparison into comparisons.
  • Program:exit only checks validated.has(side.variable) — no ordering, dominance, or branch-reachability check against the comparison node.

So a guard that appears after the unguarded use, or only in a branch that doesn't actually protect the comparison, is treated as full validation and the diagnostic is suppressed:

const d = new Date(input);
if (d > threshold) { doIt(); }              // <-- unguarded here, should be flagged
if (Number.isNaN(d.getTime())) { return; }  // check comes too late to help the line above
const d = new Date(input);
if (someUnrelatedFlag) {
  if (Number.isNaN(d.getTime())) { return; } // only reachable when someUnrelatedFlag is true
}
if (d > threshold) { doIt(); }                // still unguarded on the other path, not flagged

Both are currently silently accepted by the rule (verified by reading the implementation; no live corpus occurrence was found in actions/setup/js/**/*.cjs today — the codebase's actual new Date(...) comparisons, e.g. actions/setup/js/check_rate_limit.cjs:161 and actions/setup/js/check_runs_helpers.cjs:43, have no Number.isNaN guard at all in those files, so the rule correctly flags them as-is). This is the same class of soundness gap previously found and fixed/tracked in sibling rules in this factory (no-unsafe-catch-error-property #42189, no-unsafe-promise-catch-error-property #42915: "whole-block/branch-order false negative" — presence-anywhere-in-scope instead of control-flow-ordered validation).

Suggested fix

At minimum, require the validating Number.isNaN(...) call's node range to start before the comparison node's range for that variable (cheap, catches the reordered case). Better: reuse or extend the ordering/ancestor-walk approach already used by try-catch-rule-utils.ts's isInsideTryBlock/crossedDeferredBoundary helpers elsewhere in this factory to check the guard's containing if actually dominates (encloses or precedes-and-returns-from) the comparison's statement, not just "appears somewhere in the Program".

Acceptance criteria

  • Add a failing-then-passing test: guard written after the comparison in the same block is still reported as invalid.
  • Add a test: guard only present in a sibling/unrelated conditional branch (not guaranteed to execute before the comparison) is still reported as invalid.
  • Keep the existing valid cases (guard-before-use in same or different function scopes) passing unchanged.
  • No new false positives on actions/setup/js/**/*.cjs.

Generated by 🤖 ESLint Refiner · agent · 224.5 AIC · ⌖ 40.7 AIC · ⊞ 4.9K ·

  • expires on Aug 15, 2026, 9:40 PM UTC-08:00

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions