Skip to content

Commit 3cf63a1

Browse files
ss-vibe-bot[bot]Vibe Botclaude
authored
JS-1113 Fix FP on S7759 for polyfill fallback using Date#getTime() for Date.now() (#6333)
Co-authored-by: Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 47921c7 commit 3cf63a1

File tree

12 files changed

+337
-43
lines changed

12 files changed

+337
-43
lines changed

its/ruling/src/test/expected/jsts/TypeScript/javascript-S7759.json

Lines changed: 0 additions & 8 deletions
This file was deleted.

its/ruling/src/test/expected/jsts/TypeScript/typescript-S7759.json

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
{
22
"ace:lib/ace/mode/javascript/jshint.js": [
3-
157,
4-
3188
3+
157
54
],
65
"ace:src/background_tokenizer.js": [
76
56
8-
],
9-
"ace:tool/perf-test.html": [
10-
61
117
]
128
}

its/ruling/src/test/expected/jsts/es5-shim/javascript-S7759.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

its/ruling/src/test/expected/jsts/qunit/javascript-S7759.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,5 @@
22
"qunit:reporter/diff.js": [
33
62,
44
618
5-
],
6-
"qunit:src/core/utilities.js": [
7-
7
8-
],
9-
"qunit:test/autostart.html": [
10-
16
115
]
126
}

its/ruling/src/test/expected/jsts/rxjs/typescript-S7759.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

its/ruling/src/test/expected/jsts/underscore/javascript-S7759.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/jsts/src/rules/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,6 @@ SonarJS uses some rules are not shipped in this ESLint plugin to avoid duplicati
557557
| [S7756](https://sonarsource.github.io/rspec/#/rspec/S7756/javascript) | [unicorn/prefer-blob-reading-methods](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-blob-reading-methods.md) |
558558
| [S7757](https://sonarsource.github.io/rspec/#/rspec/S7757/javascript) | [unicorn/prefer-class-fields](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-class-fields.md) |
559559
| [S7758](https://sonarsource.github.io/rspec/#/rspec/S7758/javascript) | [unicorn/prefer-code-point](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-code-point.md) |
560-
| [S7759](https://sonarsource.github.io/rspec/#/rspec/S7759/javascript) | [unicorn/prefer-date-now](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-date-now.md) |
561560
| [S7760](https://sonarsource.github.io/rspec/#/rspec/S7760/javascript) | [unicorn/prefer-default-parameters](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-default-parameters.md) |
562561
| [S7761](https://sonarsource.github.io/rspec/#/rspec/S7761/javascript) | [unicorn/prefer-dom-node-dataset](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-dom-node-dataset.md) |
563562
| [S7762](https://sonarsource.github.io/rspec/#/rspec/S7762/javascript) | [unicorn/prefer-dom-node-remove](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-dom-node-remove.md) |
@@ -659,6 +658,7 @@ The following rules are used in SonarJS but not available in this ESLint plugin.
659658
| [S7727](https://sonarsource.github.io/rspec/#/rspec/S7727/javascript) | [unicorn/no-array-callback-reference](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/no-array-callback-reference.md) |
660659
| [S7728](https://sonarsource.github.io/rspec/#/rspec/S7728/javascript) | [unicorn/no-array-for-each](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/no-array-for-each.md) |
661660
| [S7755](https://sonarsource.github.io/rspec/#/rspec/S7755/javascript) | [unicorn/prefer-at](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-at.md) |
661+
| [S7759](https://sonarsource.github.io/rspec/#/rspec/S7759/javascript) | [unicorn/prefer-date-now](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-date-now.md) |
662662
| [S7763](https://sonarsource.github.io/rspec/#/rspec/S7763/javascript) | [unicorn/prefer-export-from](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-export-from.md) |
663663

664664
<!--- end decorated rules -->
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* SonarQube JavaScript Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
// https://sonarsource.github.io/rspec/#/rspec/S7759/javascript
18+
19+
import type { Rule } from 'eslint';
20+
import type estree from 'estree';
21+
import { generateMeta, interceptReport, isMemberExpression } from '../helpers/index.js';
22+
import * as meta from './generated-meta.js';
23+
24+
/**
25+
* Decorates the unicorn/prefer-date-now rule to filter out reports where
26+
* `new Date().getTime()`, `+(new Date())`, or similar patterns are used
27+
* inside a polyfill fallback that checks for `Date.now` availability.
28+
*/
29+
export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
30+
return interceptReport(
31+
{
32+
...rule,
33+
meta: generateMeta(meta, rule.meta),
34+
},
35+
(context, descriptor) => {
36+
const node = (descriptor as { node?: estree.Node }).node;
37+
if (!node) {
38+
context.report(descriptor);
39+
return;
40+
}
41+
42+
// Check if the node is inside a polyfill pattern
43+
if (isInsidePolyfillPattern(node, context)) {
44+
return; // Suppress the report
45+
}
46+
47+
context.report(descriptor);
48+
},
49+
);
50+
}
51+
52+
/**
53+
* Checks if the flagged node is inside a Date.now polyfill pattern.
54+
*/
55+
function isInsidePolyfillPattern(node: estree.Node, context: Rule.RuleContext): boolean {
56+
const ancestors = context.sourceCode.getAncestors(node);
57+
58+
for (const ancestor of ancestors) {
59+
// Pattern 1: Date.now || function() { return new Date().getTime(); }
60+
if (isLogicalOrPolyfill(node, ancestor, ancestors)) {
61+
return true;
62+
}
63+
64+
// Pattern 2: Date.now ? Date.now() : +(new Date())
65+
if (isTernaryPolyfill(node, ancestor, ancestors)) {
66+
return true;
67+
}
68+
69+
// Pattern 3: if (!Date.now) { Date.now = function() { ... }; }
70+
if (isIfStatementPolyfill(node, ancestor, ancestors)) {
71+
return true;
72+
}
73+
}
74+
75+
return false;
76+
}
77+
78+
/**
79+
* Checks for LogicalExpression pattern: Date.now || fallback
80+
* The flagged node should be in the right (fallback) branch.
81+
*/
82+
function isLogicalOrPolyfill(
83+
node: estree.Node,
84+
ancestor: estree.Node,
85+
ancestors: estree.Node[],
86+
): boolean {
87+
if (ancestor.type !== 'LogicalExpression' || ancestor.operator !== '||') {
88+
return false;
89+
}
90+
91+
// Check if left operand is Date.now
92+
if (!isDateNow(ancestor.left)) {
93+
return false;
94+
}
95+
96+
// Check if flagged node is in the right operand (fallback branch)
97+
return isDescendantOf(node, ancestor.right, ancestors);
98+
}
99+
100+
/**
101+
* Checks for ConditionalExpression pattern: Date.now ? Date.now() : fallback
102+
* The flagged node should be in the alternate (fallback) branch.
103+
*/
104+
function isTernaryPolyfill(
105+
node: estree.Node,
106+
ancestor: estree.Node,
107+
ancestors: estree.Node[],
108+
): boolean {
109+
if (ancestor.type !== 'ConditionalExpression') {
110+
return false;
111+
}
112+
113+
// Check if test references Date.now (truthy check)
114+
if (!isDateNow(ancestor.test)) {
115+
return false;
116+
}
117+
118+
// Check if flagged node is in the alternate (fallback) branch
119+
return isDescendantOf(node, ancestor.alternate, ancestors);
120+
}
121+
122+
/**
123+
* Checks for IfStatement pattern: if (!Date.now) { Date.now = ...; }
124+
* The flagged node should be inside the consequent block that assigns to Date.now.
125+
*/
126+
function isIfStatementPolyfill(
127+
node: estree.Node,
128+
ancestor: estree.Node,
129+
ancestors: estree.Node[],
130+
): boolean {
131+
if (ancestor.type !== 'IfStatement') {
132+
return false;
133+
}
134+
135+
// Check if test is !Date.now
136+
const { test } = ancestor;
137+
if (test.type !== 'UnaryExpression' || test.operator !== '!') {
138+
return false;
139+
}
140+
141+
if (!isDateNow(test.argument)) {
142+
return false;
143+
}
144+
145+
// Check if consequent contains an assignment to Date.now
146+
if (!containsDateNowAssignment(ancestor.consequent)) {
147+
return false;
148+
}
149+
150+
// Check if flagged node is in the consequent (not in the alternate/else branch)
151+
return isDescendantOf(node, ancestor.consequent, ancestors);
152+
}
153+
154+
/**
155+
* Checks if a node is the `Date.now` member expression.
156+
*/
157+
function isDateNow(node: estree.Node): boolean {
158+
return isMemberExpression(node, 'Date', 'now');
159+
}
160+
161+
/**
162+
* Checks if a node or its descendants contain an assignment to Date.now.
163+
*/
164+
function containsDateNowAssignment(node: estree.Node): boolean {
165+
if (node.type === 'ExpressionStatement') {
166+
return containsDateNowAssignment(node.expression);
167+
}
168+
169+
if (node.type === 'AssignmentExpression') {
170+
return isDateNow(node.left);
171+
}
172+
173+
if (node.type === 'BlockStatement') {
174+
return node.body.some(stmt => containsDateNowAssignment(stmt));
175+
}
176+
177+
return false;
178+
}
179+
180+
/**
181+
* Checks if `node` is a descendant of `potentialAncestor` using the ancestors array.
182+
*/
183+
function isDescendantOf(
184+
node: estree.Node,
185+
potentialAncestor: estree.Node,
186+
ancestors: estree.Node[],
187+
): boolean {
188+
// If node is exactly potentialAncestor, it's a descendant (trivially)
189+
if (node === potentialAncestor) {
190+
return true;
191+
}
192+
193+
// Walk up from node through ancestors to see if we pass through potentialAncestor
194+
for (const anc of ancestors) {
195+
if (anc === potentialAncestor) {
196+
return true;
197+
}
198+
}
199+
200+
return false;
201+
}

packages/jsts/src/rules/S7759/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
1717
import { rules } from '../external/unicorn.js';
18-
export const rule = rules['prefer-date-now'];
18+
import { decorate } from './decorator.js';
19+
20+
export const rule = decorate(rules['prefer-date-now']);

0 commit comments

Comments
 (0)