Skip to content

Commit 10c9229

Browse files
committed
chore(eslint): create a random id gen rule finder
1 parent ed0a0fa commit 10c9229

File tree

4 files changed

+293
-0
lines changed

4 files changed

+293
-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: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
'use strict';
2+
3+
/**
4+
* @fileoverview Rule to enforce wrapping random/time APIs with runInRandomSafeContext
5+
*
6+
* This rule detects uses of APIs that generate random values or time-based values
7+
* and ensures they are wrapped with `runInRandomSafeContext()` 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 runInRandomSafeContext
12+
const UNSAFE_MEMBER_CALLS = [
13+
{ object: 'Date', property: 'now' },
14+
{ object: 'Math', property: 'random' },
15+
{ object: 'performance', property: 'now' },
16+
{ object: 'crypto', property: 'randomUUID' },
17+
{ object: 'crypto', property: 'getRandomValues' },
18+
];
19+
20+
module.exports = {
21+
meta: {
22+
type: 'problem',
23+
docs: {
24+
description:
25+
'Enforce wrapping random/time APIs (Date.now, Math.random, performance.now, crypto.randomUUID) with runInRandomSafeContext',
26+
category: 'Best Practices',
27+
recommended: true,
28+
},
29+
fixable: null,
30+
schema: [],
31+
messages: {
32+
unsafeRandomApi:
33+
'{{ api }} should be wrapped with runInRandomSafeContext() to ensure safe random/time value generation. Use: runInRandomSafeContext(() => {{ api }}). You can disable this rule with an eslint-disable comment if this usage is intentional.',
34+
},
35+
},
36+
create: function (context) {
37+
/**
38+
* Check if a node is inside a runInRandomSafeContext call
39+
*/
40+
function isInsideRunInRandomSafeContext(node) {
41+
let current = node.parent;
42+
43+
while (current) {
44+
// Check if we're inside a callback passed to runInRandomSafeContext
45+
if (
46+
current.type === 'CallExpression' &&
47+
current.callee.type === 'Identifier' &&
48+
current.callee.name === 'runInRandomSafeContext'
49+
) {
50+
return true;
51+
}
52+
53+
// Also check for arrow functions or regular functions passed to runInRandomSafeContext
54+
if (
55+
(current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression') &&
56+
current.parent?.type === 'CallExpression' &&
57+
current.parent.callee.type === 'Identifier' &&
58+
current.parent.callee.name === 'runInRandomSafeContext'
59+
) {
60+
return true;
61+
}
62+
63+
current = current.parent;
64+
}
65+
66+
return false;
67+
}
68+
69+
/**
70+
* Check if a node is inside the safeRandomGeneratorRunner.ts file (the definition file)
71+
*/
72+
function isInSafeRandomGeneratorRunner(_node) {
73+
const filename = context.getFilename();
74+
return filename.includes('safeRandomGeneratorRunner');
75+
}
76+
77+
return {
78+
CallExpression(node) {
79+
// Skip if we're in the safeRandomGeneratorRunner.ts file itself
80+
if (isInSafeRandomGeneratorRunner(node)) {
81+
return;
82+
}
83+
84+
// Check for member expression calls like Date.now(), Math.random(), etc.
85+
if (node.callee.type === 'MemberExpression') {
86+
const callee = node.callee;
87+
88+
// Get the object name (e.g., 'Date', 'Math', 'performance', 'crypto')
89+
let objectName = null;
90+
if (callee.object.type === 'Identifier') {
91+
objectName = callee.object.name;
92+
}
93+
94+
// Get the property name (e.g., 'now', 'random', 'randomUUID')
95+
let propertyName = null;
96+
if (callee.property.type === 'Identifier') {
97+
propertyName = callee.property.name;
98+
} else if (callee.computed && callee.property.type === 'Literal') {
99+
propertyName = callee.property.value;
100+
}
101+
102+
if (!objectName || !propertyName) {
103+
return;
104+
}
105+
106+
// Check if this is one of the unsafe APIs
107+
const isUnsafeApi = UNSAFE_MEMBER_CALLS.some(
108+
api => api.object === objectName && api.property === propertyName,
109+
);
110+
111+
if (isUnsafeApi && !isInsideRunInRandomSafeContext(node)) {
112+
context.report({
113+
node,
114+
messageId: 'unsafeRandomApi',
115+
data: {
116+
api: `${objectName}.${propertyName}()`,
117+
},
118+
});
119+
}
120+
}
121+
},
122+
};
123+
},
124+
};
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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 runInRandomSafeContext - arrow function
17+
{
18+
code: 'runInRandomSafeContext(() => Date.now())',
19+
},
20+
{
21+
code: 'runInRandomSafeContext(() => Math.random())',
22+
},
23+
{
24+
code: 'runInRandomSafeContext(() => performance.now())',
25+
},
26+
{
27+
code: 'runInRandomSafeContext(() => crypto.randomUUID())',
28+
},
29+
{
30+
code: 'runInRandomSafeContext(() => crypto.getRandomValues(new Uint8Array(16)))',
31+
},
32+
// Wrapped with runInRandomSafeContext - regular function
33+
{
34+
code: 'runInRandomSafeContext(function() { return Date.now(); })',
35+
},
36+
// Nested inside runInRandomSafeContext
37+
{
38+
code: 'runInRandomSafeContext(() => { const x = Date.now(); return x + Math.random(); })',
39+
},
40+
// Expression inside runInRandomSafeContext
41+
{
42+
code: 'runInRandomSafeContext(() => 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: 'unsafeRandomApi',
65+
data: { api: 'Date.now()' },
66+
},
67+
],
68+
},
69+
// Direct Math.random() calls
70+
{
71+
code: 'const random = Math.random()',
72+
errors: [
73+
{
74+
messageId: 'unsafeRandomApi',
75+
data: { api: 'Math.random()' },
76+
},
77+
],
78+
},
79+
// Direct performance.now() calls
80+
{
81+
code: 'const perf = performance.now()',
82+
errors: [
83+
{
84+
messageId: 'unsafeRandomApi',
85+
data: { api: 'performance.now()' },
86+
},
87+
],
88+
},
89+
// Direct crypto.randomUUID() calls
90+
{
91+
code: 'const uuid = crypto.randomUUID()',
92+
errors: [
93+
{
94+
messageId: 'unsafeRandomApi',
95+
data: { api: 'crypto.randomUUID()' },
96+
},
97+
],
98+
},
99+
// Direct crypto.getRandomValues() calls
100+
{
101+
code: 'const bytes = crypto.getRandomValues(new Uint8Array(16))',
102+
errors: [
103+
{
104+
messageId: 'unsafeRandomApi',
105+
data: { api: 'crypto.getRandomValues()' },
106+
},
107+
],
108+
},
109+
// Inside a function but not wrapped
110+
{
111+
code: 'function getTime() { return Date.now(); }',
112+
errors: [
113+
{
114+
messageId: 'unsafeRandomApi',
115+
data: { api: 'Date.now()' },
116+
},
117+
],
118+
},
119+
// Inside an arrow function but not wrapped with runInRandomSafeContext
120+
{
121+
code: 'const getTime = () => Date.now()',
122+
errors: [
123+
{
124+
messageId: 'unsafeRandomApi',
125+
data: { api: 'Date.now()' },
126+
},
127+
],
128+
},
129+
// Inside someOtherWrapper
130+
{
131+
code: 'someOtherWrapper(() => Date.now())',
132+
errors: [
133+
{
134+
messageId: 'unsafeRandomApi',
135+
data: { api: 'Date.now()' },
136+
},
137+
],
138+
},
139+
// Multiple violations
140+
{
141+
code: 'const a = Date.now(); const b = Math.random();',
142+
errors: [
143+
{
144+
messageId: 'unsafeRandomApi',
145+
data: { api: 'Date.now()' },
146+
},
147+
{
148+
messageId: 'unsafeRandomApi',
149+
data: { api: 'Math.random()' },
150+
},
151+
],
152+
},
153+
],
154+
});
155+
});
156+
});
157+

0 commit comments

Comments
 (0)