Skip to content

Require isNaN(getTime()) guard to precede and dominate the date comparison - #51532

Merged
pelikhan merged 3 commits into
mainfrom
copilot/require-invalid-date-check-before-compare
Aug 9, 2026
Merged

Require isNaN(getTime()) guard to precede and dominate the date comparison#51532
pelikhan merged 3 commits into
mainfrom
copilot/require-invalid-date-check-before-compare

Conversation

Copilot AI commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

require-invalid-date-check-before-compare marked a variable validated whenever a Number.isNaN(x.getTime()) call existed anywhere in the traversal, with no positional or reachability check. A guard written after the risky comparison, or nested in a branch that doesn't protect it, silently suppressed the diagnostic.

const d = new Date(input);
if (d > threshold) { doIt(); }              // was not flagged
if (Number.isNaN(d.getTime())) { return; }  // guard comes too late to help

const e = new Date(input);
if (someUnrelatedFlag) {
  if (Number.isNaN(e.getTime())) { return; } // only runs on one path
}
if (e > threshold) { doIt(); }               // was not flagged

Changes

  • validated: Set<Variable>guards: Map<Variable, Node[][]> — each guard is recorded with its ancestor path (sourceCode.getAncestors, matching the pattern in sibling rules). Comparisons record their path too.
  • guardDominatesComparison(guardPath, comparisonPath) — a guard counts only when it ends before the comparison starts in source order and no conditional edge is taken on the guard's path below the deepest node the two share.
  • isConditionalEdge(parent, child) — classifies edges that aren't guaranteed to execute: if/ternary consequent & alternate, right operand of &&/||, switch-case bodies, catch handlers, loop bodies, function bodies. Always-executed positions (if test, left operand of a logical expression, try block) are excluded, which is what keeps !Number.isNaN(d.getTime()) && d > t and guard-then-use valid.
  • Program:exit filters sides through isValidatedBefore(variable, comparisonPath) instead of a set membership test.

Tests

Invalid: guard after the comparison; guard inside a sibling if branch; guard inside a for…of body.
Valid (new): guard in the if test with the comparison in the else if; guard preceding a loop that contains the comparison.

Existing valid/invalid cases are unchanged, and the corpus lint output for actions/setup/js/**/*.cjs reports the same two occurrences as before.

Note: the pre-existing failures in require-fs-io-try-catch.test.ts also reproduce on the base commit and are untouched here.

Copilot AI and others added 2 commits August 9, 2026 06:02
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…rison

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix validation logic for require-invalid-date-check-before-compare rule Require isNaN(getTime()) guard to precede and dominate the date comparison Aug 9, 2026
Copilot AI requested a review from pelikhan August 9, 2026 06:10
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

PR Triage

  • Category: bug
  • Risk: medium
  • Priority: medium
  • Score: 48/100 (impact 25 + urgency 15 + quality 8)
  • Recommended action: defer
    Batch: lockfile-regen-cluster\n
    Fixes false-negative in security lint rule (date compare), draft, blocked, no CI yet.

Generated by 🔧 PR Triage Agent · auto · 58.3 AIC · ⌖ 2.48 AIC · ⊞ 8K ·

@pelikhan
pelikhan marked this pull request as ready for review August 9, 2026 11:03
Copilot AI balanced review requested due to automatic review settings August 9, 2026 11:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the invalid-date ESLint rule to consider guard ordering and reachability. However, the proposed ancestor-based analysis still misses bypass paths, mishandles switch cases, and includes unrelated workflow lockfile regeneration.

Changes:

  • Records guard and comparison ancestor paths.
  • Adds ordering/reachability tests.
  • Regenerates numerous workflow lockfiles with additional safe-output counters.
Show a summary per file
File Description
eslint-factory/src/rules/require-invalid-date-check-before-compare.ts Adds guard dominance analysis.
eslint-factory/src/rules/require-invalid-date-check-before-compare.test.ts Adds ordering and branch tests.
.github/workflows/*.lock.yml (all listed lockfiles) Adds generated safe-output counters; unrelated to the stated ESLint fix.

Review details

Tip

Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 285/285 changed files
  • Comments generated: 3
  • Review effort level: Balanced

create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }}
created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }}
process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }}
Comment on lines +78 to +79
case AST_NODE_TYPES.SwitchCase:
return parent.consequent.includes(child as TSESTree.Statement);
Comment on lines +109 to +115
let divergence = 0;
while (divergence < guardPath.length && divergence < comparisonPath.length && guardPath[divergence] === comparisonPath[divergence]) {
divergence++;
}
// Guards and comparisons always share the Program node, so `divergence` is normally at least 1.
for (let i = Math.max(divergence, 1); i < guardPath.length; i++) {
if (isConditionalEdge(guardPath[i - 1], guardPath[i])) return false;
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

PR #51532's pre-fetched diff and metadata contain only 100 generated .lock.yml workflow files (mechanical additions of new safe-outputs step outputs), with no non-generated source files (e.g., the actual ESLint rule change described in the PR body) present to review. Per instructions to skip generated/lock files, there is nothing eligible for a ponytail over-engineering review.

Generated by Ponytail Reviewer for #51532

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • api.individual.githubcopilot.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "api.individual.githubcopilot.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer

@pelikhan
pelikhan merged commit 9ef756f into main Aug 9, 2026
45 checks passed
@pelikhan
pelikhan deleted the copilot/require-invalid-date-check-before-compare branch August 9, 2026 11:23

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 110.2 AIC · ⌖ 10.6 AIC · ⊞ 7.1K
Comment /matt to run again

case AST_NODE_TYPES.SwitchCase:
return parent.consequent.includes(child as TSESTree.Statement);
case AST_NODE_TYPES.TryStatement:
return child === parent.handler;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] TryStatement only marks the catch handler as a conditional edge, but the finally block (parent.finalizer) is not checked — a guard inside a try body is also conditional if code above it can throw. Consider also treating child === parent.finalizer as a conditional edge.

💡 Missed scenario
const d = new Date(input);
try {
  somethingThatMightThrow();
  if (Number.isNaN(d.getTime())) { return; } // skipped if throw above
} catch (e) {}
if (d > threshold) { doIt(); } // still fires with invalid date

@copilot please address this.

function guardDominatesComparison(guardPath: TSESTree.Node[], comparisonPath: TSESTree.Node[]): boolean {
const guard = guardPath[guardPath.length - 1];
const comparison = comparisonPath[comparisonPath.length - 1];
if (guard.range[1] > comparison.range[0]) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The range-based ordering check (guard.range[1] > comparison.range[0]) assumes both nodes have valid ranges, but ESLint nodes can have range as undefined if the parser is not configured with range: true. This would throw at runtime rather than fail gracefully.

💡 Defensive fix

Add a guard:

if (!guard.range || !comparison.range) return false; // can't determine order without ranges
if (guard.range[1] > comparison.range[0]) return false;

Or verify in test configuration that range is always enabled (the RuleTester default enables it, but real-world consumer configs may not).

@copilot please address this.

divergence++;
}
// Guards and comparisons always share the Program node, so `divergence` is normally at least 1.
for (let i = Math.max(divergence, 1); i < guardPath.length; i++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] Math.max(divergence, 1) skips index 0 of guardPath when the paths don't diverge until after index 0, but it also skips the check when divergence === 0 (guard and comparison share no common prefix beyond the implicit assumption). If divergence is 0 (e.g. two paths built from different root traversals), the loop starts at index 1 and the edge between index 0 and 1 is silently skipped. Add a test case where two paths share no common node to confirm this is intentionally safe.

@copilot please address this.

});
});

it("invalid: guard nested in an unrelated conditional branch does not protect the comparison", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test name says "unrelated conditional branch" but the two cases cover if and for...of — there is no test for while, do...while, &&/|| short-circuit, or a switch case body. Adding at least one while loop and one logical-AND case as invalid examples would close the most likely regression paths.

💡 Suggested additions
// while loop guard — should be invalid
{ code: `const d = new Date(x); while (cond) { if (Number.isNaN(d.getTime())) return; } if (d > t) {}`, errors: [...] },
// logical-AND right side — should be invalid
{ code: `const d = new Date(x); true && (Number.isNaN(d.getTime()) ? true : false); if (d > t) {}`, errors: [...] },

@copilot please address this.

case AST_NODE_TYPES.LogicalExpression:
return child === parent.right;
case AST_NODE_TYPES.SwitchCase:
return parent.consequent.includes(child as TSESTree.Statement);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] SwitchCase checks whether child is in parent.consequent (the statements of one case), but the SwitchStatement itself — which holds the discriminant and the array of cases — is never matched. This means a guard in a switch discriminant position (switch (Number.isNaN(d.getTime()))) would correctly fall through to the default: return false branch and not be treated as conditional. That's the correct outcome, but there is no test verifying it. A guard inside a case body is already covered by SwitchCase. Verify with a test that a guard in a case consequent is correctly rejected.

@copilot please address this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants