Skip to content

Commit 0467fa5

Browse files
committed
chore: bring back the eslint rule
1 parent bb7c44b commit 0467fa5

File tree

9 files changed

+352
-0
lines changed

9 files changed

+352
-0
lines changed

packages/core/.eslintrc.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
module.exports = {
22
extends: ['../../.eslintrc.js'],
33
ignorePatterns: ['rollup.npm.config.mjs'],
4+
rules: {
5+
'@sentry-internal/sdk/no-unsafe-random-apis': 'error',
6+
},
7+
overrides: [
8+
{
9+
files: ['test/**/*.ts', 'test/**/*.tsx'],
10+
rules: {
11+
'@sentry-internal/sdk/no-unsafe-random-apis': 'off',
12+
},
13+
},
14+
],
415
};

packages/eslint-plugin-sdk/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ module.exports = {
1515
'no-regexp-constructor': require('./rules/no-regexp-constructor'),
1616
'no-focused-tests': require('./rules/no-focused-tests'),
1717
'no-skipped-tests': require('./rules/no-skipped-tests'),
18+
'no-unsafe-random-apis': require('./rules/no-unsafe-random-apis'),
1819
},
1920
};
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
'use strict';
2+
3+
/**
4+
* @fileoverview Rule to enforce wrapping random/time APIs with withRandomSafeContext
5+
*
6+
* This rule detects uses of APIs that generate random values or time-based values
7+
* and ensures they are wrapped with `withRandomSafeContext()` to ensure safe
8+
* random number generation in certain contexts (e.g., React Server Components with caching).
9+
*/
10+
11+
// APIs that should be wrapped with withRandomSafeContext, with their specific messages
12+
const UNSAFE_MEMBER_CALLS = [
13+
{
14+
object: 'Date',
15+
property: 'now',
16+
messageId: 'unsafeDateNow',
17+
},
18+
{
19+
object: 'Math',
20+
property: 'random',
21+
messageId: 'unsafeMathRandom',
22+
},
23+
{
24+
object: 'performance',
25+
property: 'now',
26+
messageId: 'unsafePerformanceNow',
27+
},
28+
{
29+
object: 'crypto',
30+
property: 'randomUUID',
31+
messageId: 'unsafeCryptoRandomUUID',
32+
},
33+
{
34+
object: 'crypto',
35+
property: 'getRandomValues',
36+
messageId: 'unsafeCryptoGetRandomValues',
37+
},
38+
];
39+
40+
module.exports = {
41+
meta: {
42+
type: 'problem',
43+
docs: {
44+
description:
45+
'Enforce wrapping random/time APIs (Date.now, Math.random, performance.now, crypto.randomUUID) with withRandomSafeContext',
46+
category: 'Best Practices',
47+
recommended: true,
48+
},
49+
fixable: null,
50+
schema: [],
51+
messages: {
52+
unsafeDateNow:
53+
'`Date.now()` should be replaced with `safeDateNow()` from `@sentry/core` to ensure safe time value generation. You can disable this rule with an eslint-disable comment if this usage is intentional.',
54+
unsafeMathRandom:
55+
'`Math.random()` should be replaced with `safeMathRandom()` from `@sentry/core` to ensure safe random value generation. You can disable this rule with an eslint-disable comment if this usage is intentional.',
56+
unsafePerformanceNow:
57+
'`performance.now()` should be wrapped with `withRandomSafeContext()` to ensure safe time value generation. Use: `withRandomSafeContext(() => performance.now())`. You can disable this rule with an eslint-disable comment if this usage is intentional.',
58+
unsafeCryptoRandomUUID:
59+
'`crypto.randomUUID()` should be wrapped with `withRandomSafeContext()` to ensure safe random value generation. Use: `withRandomSafeContext(() => crypto.randomUUID())`. You can disable this rule with an eslint-disable comment if this usage is intentional.',
60+
unsafeCryptoGetRandomValues:
61+
'`crypto.getRandomValues()` should be wrapped with `withRandomSafeContext()` to ensure safe random value generation. Use: `withRandomSafeContext(() => crypto.getRandomValues(...))`. You can disable this rule with an eslint-disable comment if this usage is intentional.',
62+
},
63+
},
64+
create: function (context) {
65+
/**
66+
* Check if a node is inside a withRandomSafeContext call
67+
*/
68+
function isInsidewithRandomSafeContext(node) {
69+
let current = node.parent;
70+
71+
while (current) {
72+
// Check if we're inside a callback passed to withRandomSafeContext
73+
if (
74+
current.type === 'CallExpression' &&
75+
current.callee.type === 'Identifier' &&
76+
current.callee.name === 'withRandomSafeContext'
77+
) {
78+
return true;
79+
}
80+
81+
// Also check for arrow functions or regular functions passed to withRandomSafeContext
82+
if (
83+
(current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') &&
84+
current.parent?.type === 'CallExpression' &&
85+
current.parent.callee.type === 'Identifier' &&
86+
current.parent.callee.name === 'withRandomSafeContext'
87+
) {
88+
return true;
89+
}
90+
91+
current = current.parent;
92+
}
93+
94+
return false;
95+
}
96+
97+
/**
98+
* Check if a node is inside the safeRandomGeneratorRunner.ts file (the definition file)
99+
*/
100+
function isInSafeRandomGeneratorRunner(_node) {
101+
const filename = context.getFilename();
102+
return filename.includes('safeRandomGeneratorRunner');
103+
}
104+
105+
return {
106+
CallExpression(node) {
107+
// Skip if we're in the safeRandomGeneratorRunner.ts file itself
108+
if (isInSafeRandomGeneratorRunner(node)) {
109+
return;
110+
}
111+
112+
// Check for member expression calls like Date.now(), Math.random(), etc.
113+
if (node.callee.type === 'MemberExpression') {
114+
const callee = node.callee;
115+
116+
// Get the object name (e.g., 'Date', 'Math', 'performance', 'crypto')
117+
let objectName = null;
118+
if (callee.object.type === 'Identifier') {
119+
objectName = callee.object.name;
120+
}
121+
122+
// Get the property name (e.g., 'now', 'random', 'randomUUID')
123+
let propertyName = null;
124+
if (callee.property.type === 'Identifier') {
125+
propertyName = callee.property.name;
126+
} else if (callee.computed && callee.property.type === 'Literal') {
127+
propertyName = callee.property.value;
128+
}
129+
130+
if (!objectName || !propertyName) {
131+
return;
132+
}
133+
134+
// Check if this is one of the unsafe APIs
135+
const unsafeApi = UNSAFE_MEMBER_CALLS.find(api => api.object === objectName && api.property === propertyName);
136+
137+
if (unsafeApi && !isInsidewithRandomSafeContext(node)) {
138+
context.report({
139+
node,
140+
messageId: unsafeApi.messageId,
141+
});
142+
}
143+
}
144+
},
145+
};
146+
},
147+
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { RuleTester } from 'eslint';
2+
import { describe, test } from 'vitest';
3+
// @ts-expect-error untyped module
4+
import rule from '../../../src/rules/no-unsafe-random-apis';
5+
6+
describe('no-unsafe-random-apis', () => {
7+
test('ruleTester', () => {
8+
const ruleTester = new RuleTester({
9+
parserOptions: {
10+
ecmaVersion: 2020,
11+
},
12+
});
13+
14+
ruleTester.run('no-unsafe-random-apis', rule, {
15+
valid: [
16+
// Wrapped with withRandomSafeContext - arrow function
17+
{
18+
code: 'withRandomSafeContext(() => Date.now())',
19+
},
20+
{
21+
code: 'withRandomSafeContext(() => Math.random())',
22+
},
23+
{
24+
code: 'withRandomSafeContext(() => performance.now())',
25+
},
26+
{
27+
code: 'withRandomSafeContext(() => crypto.randomUUID())',
28+
},
29+
{
30+
code: 'withRandomSafeContext(() => crypto.getRandomValues(new Uint8Array(16)))',
31+
},
32+
// Wrapped with withRandomSafeContext - regular function
33+
{
34+
code: 'withRandomSafeContext(function() { return Date.now(); })',
35+
},
36+
// Nested inside withRandomSafeContext
37+
{
38+
code: 'withRandomSafeContext(() => { const x = Date.now(); return x + Math.random(); })',
39+
},
40+
// Expression inside withRandomSafeContext
41+
{
42+
code: 'withRandomSafeContext(() => Date.now() / 1000)',
43+
},
44+
// Other unrelated calls should be fine
45+
{
46+
code: 'const x = someObject.now()',
47+
},
48+
{
49+
code: 'const x = Date.parse("2021-01-01")',
50+
},
51+
{
52+
code: 'const x = Math.floor(5.5)',
53+
},
54+
{
55+
code: 'const x = performance.mark("test")',
56+
},
57+
],
58+
invalid: [
59+
// Direct Date.now() calls
60+
{
61+
code: 'const time = Date.now()',
62+
errors: [
63+
{
64+
messageId: 'unsafeDateNow',
65+
},
66+
],
67+
},
68+
// Direct Math.random() calls
69+
{
70+
code: 'const random = Math.random()',
71+
errors: [
72+
{
73+
messageId: 'unsafeMathRandom',
74+
},
75+
],
76+
},
77+
// Direct performance.now() calls
78+
{
79+
code: 'const perf = performance.now()',
80+
errors: [
81+
{
82+
messageId: 'unsafePerformanceNow',
83+
},
84+
],
85+
},
86+
// Direct crypto.randomUUID() calls
87+
{
88+
code: 'const uuid = crypto.randomUUID()',
89+
errors: [
90+
{
91+
messageId: 'unsafeCryptoRandomUUID',
92+
},
93+
],
94+
},
95+
// Direct crypto.getRandomValues() calls
96+
{
97+
code: 'const bytes = crypto.getRandomValues(new Uint8Array(16))',
98+
errors: [
99+
{
100+
messageId: 'unsafeCryptoGetRandomValues',
101+
},
102+
],
103+
},
104+
// Inside a function but not wrapped
105+
{
106+
code: 'function getTime() { return Date.now(); }',
107+
errors: [
108+
{
109+
messageId: 'unsafeDateNow',
110+
},
111+
],
112+
},
113+
// Inside an arrow function but not wrapped with withRandomSafeContext
114+
{
115+
code: 'const getTime = () => Date.now()',
116+
errors: [
117+
{
118+
messageId: 'unsafeDateNow',
119+
},
120+
],
121+
},
122+
// Inside someOtherWrapper
123+
{
124+
code: 'someOtherWrapper(() => Date.now())',
125+
errors: [
126+
{
127+
messageId: 'unsafeDateNow',
128+
},
129+
],
130+
},
131+
// Multiple violations
132+
{
133+
code: 'const a = Date.now(); const b = Math.random();',
134+
errors: [
135+
{
136+
messageId: 'unsafeDateNow',
137+
},
138+
{
139+
messageId: 'unsafeMathRandom',
140+
},
141+
],
142+
},
143+
],
144+
});
145+
});
146+
});

packages/nextjs/.eslintrc.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ module.exports = {
77
jsx: true,
88
},
99
extends: ['../../.eslintrc.js'],
10+
rules: {
11+
'@sentry-internal/sdk/no-unsafe-random-apis': 'error',
12+
},
1013
overrides: [
1114
{
1215
files: ['scripts/**/*.ts'],
@@ -27,5 +30,11 @@ module.exports = {
2730
globalThis: 'readonly',
2831
},
2932
},
33+
{
34+
files: ['test/**/*.ts', 'test/**/*.tsx'],
35+
rules: {
36+
'@sentry-internal/sdk/no-unsafe-random-apis': 'off',
37+
},
38+
},
3039
],
3140
};

packages/node-core/.eslintrc.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,14 @@ module.exports = {
55
extends: ['../../.eslintrc.js'],
66
rules: {
77
'@sentry-internal/sdk/no-class-field-initializers': 'off',
8+
'@sentry-internal/sdk/no-unsafe-random-apis': 'error',
89
},
10+
overrides: [
11+
{
12+
files: ['test/**/*.ts', 'test/**/*.tsx'],
13+
rules: {
14+
'@sentry-internal/sdk/no-unsafe-random-apis': 'off',
15+
},
16+
},
17+
],
918
};

packages/node/.eslintrc.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,14 @@ module.exports = {
55
extends: ['../../.eslintrc.js'],
66
rules: {
77
'@sentry-internal/sdk/no-class-field-initializers': 'off',
8+
'@sentry-internal/sdk/no-unsafe-random-apis': 'error',
89
},
10+
overrides: [
11+
{
12+
files: ['test/**/*.ts', 'test/**/*.tsx'],
13+
rules: {
14+
'@sentry-internal/sdk/no-unsafe-random-apis': 'off',
15+
},
16+
},
17+
],
918
};

packages/opentelemetry/.eslintrc.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,15 @@ module.exports = {
33
node: true,
44
},
55
extends: ['../../.eslintrc.js'],
6+
rules: {
7+
'@sentry-internal/sdk/no-unsafe-random-apis': 'error',
8+
},
9+
overrides: [
10+
{
11+
files: ['test/**/*.ts', 'test/**/*.tsx'],
12+
rules: {
13+
'@sentry-internal/sdk/no-unsafe-random-apis': 'off',
14+
},
15+
},
16+
],
617
};

0 commit comments

Comments
 (0)