Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

6 changes: 1 addition & 5 deletions its/ruling/src/test/expected/jsts/ace/javascript-S7759.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
{
"ace:lib/ace/mode/javascript/jshint.js": [
157,
3188
157
],
"ace:src/background_tokenizer.js": [
56
],
"ace:tool/perf-test.html": [
61
]
}

This file was deleted.

6 changes: 0 additions & 6 deletions its/ruling/src/test/expected/jsts/qunit/javascript-S7759.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,5 @@
"qunit:reporter/diff.js": [
62,
618
],
"qunit:src/core/utilities.js": [
7
],
"qunit:test/autostart.html": [
16
]
}
5 changes: 0 additions & 5 deletions its/ruling/src/test/expected/jsts/rxjs/typescript-S7759.json

This file was deleted.

This file was deleted.

2 changes: 1 addition & 1 deletion packages/jsts/src/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,6 @@ SonarJS uses some rules are not shipped in this ESLint plugin to avoid duplicati
| [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) |
| [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) |
| [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) |
| [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) |
| [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) |
| [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) |
| [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) |
Expand Down Expand Up @@ -659,6 +658,7 @@ The following rules are used in SonarJS but not available in this ESLint plugin.
| [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) |
| [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) |
| [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) |
| [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) |
| [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) |

<!--- end decorated rules -->
201 changes: 201 additions & 0 deletions packages/jsts/src/rules/S7759/decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
// https://sonarsource.github.io/rspec/#/rspec/S7759/javascript

import type { Rule } from 'eslint';
import type estree from 'estree';
import { generateMeta, interceptReport, isMemberExpression } from '../helpers/index.js';
import * as meta from './generated-meta.js';

/**
* Decorates the unicorn/prefer-date-now rule to filter out reports where
* `new Date().getTime()`, `+(new Date())`, or similar patterns are used
* inside a polyfill fallback that checks for `Date.now` availability.
*/
export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
return interceptReport(
{
...rule,
meta: generateMeta(meta, rule.meta),
},
(context, descriptor) => {
const node = (descriptor as { node?: estree.Node }).node;
if (!node) {
context.report(descriptor);
return;
}

// Check if the node is inside a polyfill pattern
if (isInsidePolyfillPattern(node, context)) {
return; // Suppress the report
}

context.report(descriptor);
},
);
}

/**
* Checks if the flagged node is inside a Date.now polyfill pattern.
*/
function isInsidePolyfillPattern(node: estree.Node, context: Rule.RuleContext): boolean {
const ancestors = context.sourceCode.getAncestors(node);

for (const ancestor of ancestors) {
// Pattern 1: Date.now || function() { return new Date().getTime(); }
if (isLogicalOrPolyfill(node, ancestor, ancestors)) {
return true;
}

// Pattern 2: Date.now ? Date.now() : +(new Date())
if (isTernaryPolyfill(node, ancestor, ancestors)) {
return true;
}

// Pattern 3: if (!Date.now) { Date.now = function() { ... }; }
if (isIfStatementPolyfill(node, ancestor, ancestors)) {
return true;
}
}

return false;
}

/**
* Checks for LogicalExpression pattern: Date.now || fallback
* The flagged node should be in the right (fallback) branch.
*/
function isLogicalOrPolyfill(
node: estree.Node,
ancestor: estree.Node,
ancestors: estree.Node[],
): boolean {
if (ancestor.type !== 'LogicalExpression' || ancestor.operator !== '||') {
return false;
}

// Check if left operand is Date.now
if (!isDateNow(ancestor.left)) {
return false;
}

// Check if flagged node is in the right operand (fallback branch)
return isDescendantOf(node, ancestor.right, ancestors);
}

/**
* Checks for ConditionalExpression pattern: Date.now ? Date.now() : fallback
* The flagged node should be in the alternate (fallback) branch.
*/
function isTernaryPolyfill(
node: estree.Node,
ancestor: estree.Node,
ancestors: estree.Node[],
): boolean {
if (ancestor.type !== 'ConditionalExpression') {
return false;
}

// Check if test references Date.now (truthy check)
if (!isDateNow(ancestor.test)) {
return false;
}

// Check if flagged node is in the alternate (fallback) branch
return isDescendantOf(node, ancestor.alternate, ancestors);
}

/**
* Checks for IfStatement pattern: if (!Date.now) { Date.now = ...; }
* The flagged node should be inside the consequent block that assigns to Date.now.
*/
function isIfStatementPolyfill(
node: estree.Node,
ancestor: estree.Node,
ancestors: estree.Node[],
): boolean {
if (ancestor.type !== 'IfStatement') {
return false;
}

// Check if test is !Date.now
const { test } = ancestor;
if (test.type !== 'UnaryExpression' || test.operator !== '!') {
return false;
}

if (!isDateNow(test.argument)) {
return false;
}

// Check if consequent contains an assignment to Date.now
if (!containsDateNowAssignment(ancestor.consequent)) {
return false;
}

// Check if flagged node is in the consequent (not in the alternate/else branch)
return isDescendantOf(node, ancestor.consequent, ancestors);
}

/**
* Checks if a node is the `Date.now` member expression.
*/
function isDateNow(node: estree.Node): boolean {
return isMemberExpression(node, 'Date', 'now');
}

/**
* Checks if a node or its descendants contain an assignment to Date.now.
*/
function containsDateNowAssignment(node: estree.Node): boolean {
if (node.type === 'ExpressionStatement') {
return containsDateNowAssignment(node.expression);
}

if (node.type === 'AssignmentExpression') {
return isDateNow(node.left);
}

if (node.type === 'BlockStatement') {
return node.body.some(stmt => containsDateNowAssignment(stmt));
}

return false;
}

/**
* Checks if `node` is a descendant of `potentialAncestor` using the ancestors array.
*/
function isDescendantOf(
node: estree.Node,
potentialAncestor: estree.Node,
ancestors: estree.Node[],
): boolean {
// If node is exactly potentialAncestor, it's a descendant (trivially)
if (node === potentialAncestor) {
return true;
}

// Walk up from node through ancestors to see if we pass through potentialAncestor
for (const anc of ancestors) {
if (anc === potentialAncestor) {
return true;
}
}

return false;
}
4 changes: 3 additions & 1 deletion packages/jsts/src/rules/S7759/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
import { rules } from '../external/unicorn.js';
export const rule = rules['prefer-date-now'];
import { decorate } from './decorator.js';

export const rule = decorate(rules['prefer-date-now']);
4 changes: 2 additions & 2 deletions packages/jsts/src/rules/S7759/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
// https://sonarsource.github.io/rspec/#/rspec/S7759/javascript
export const implementation = 'external';
export const implementation = 'decorated';
export const eslintId = 'prefer-date-now';
export const externalPlugin = 'unicorn';
export const externalRules = [{ externalPlugin: 'unicorn', externalRule: 'prefer-date-now' }];
export const quickFixMessage = 'Replace with Date.now()';
Loading
Loading