@series-inc/rundot-syncplay 5.23.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +610 -0
- package/dist/animation.d.ts +119 -0
- package/dist/animation.js +488 -0
- package/dist/asset-cooking.d.ts +82 -0
- package/dist/asset-cooking.js +364 -0
- package/dist/authority-room.d.ts +217 -0
- package/dist/authority-room.js +701 -0
- package/dist/bot-documents.d.ts +115 -0
- package/dist/bot-documents.js +549 -0
- package/dist/bot-primitives.d.ts +152 -0
- package/dist/bot-primitives.js +472 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +53 -0
- package/dist/canonical.d.ts +12 -0
- package/dist/canonical.js +288 -0
- package/dist/certification.d.ts +455 -0
- package/dist/certification.js +1855 -0
- package/dist/chaos-certification.d.ts +56 -0
- package/dist/chaos-certification.js +1397 -0
- package/dist/cjs/animation.js +495 -0
- package/dist/cjs/asset-cooking.js +372 -0
- package/dist/cjs/authority-room.js +704 -0
- package/dist/cjs/bot-documents.js +556 -0
- package/dist/cjs/bot-primitives.js +482 -0
- package/dist/cjs/browser.js +96 -0
- package/dist/cjs/canonical.js +299 -0
- package/dist/cjs/certification.js +1860 -0
- package/dist/cjs/chaos-certification.js +1402 -0
- package/dist/cjs/collections.js +36 -0
- package/dist/cjs/collision.js +96 -0
- package/dist/cjs/commands.js +987 -0
- package/dist/cjs/config-bundle.js +491 -0
- package/dist/cjs/creator.js +30 -0
- package/dist/cjs/deterministic-primitives.js +134 -0
- package/dist/cjs/deterministic-session.js +1410 -0
- package/dist/cjs/ecs-lite.js +155 -0
- package/dist/cjs/effects-adapter.js +97 -0
- package/dist/cjs/engine-complete.js +2297 -0
- package/dist/cjs/engine-core-proof.js +70 -0
- package/dist/cjs/engine-parity-matrix.js +874 -0
- package/dist/cjs/engine-parity.js +3183 -0
- package/dist/cjs/engine-runtime.js +3141 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/events.js +247 -0
- package/dist/cjs/examples/counter-duel.js +157 -0
- package/dist/cjs/examples/fixed-arena.js +368 -0
- package/dist/cjs/index.js +229 -0
- package/dist/cjs/input-authority.js +150 -0
- package/dist/cjs/input-button.js +256 -0
- package/dist/cjs/input-timeline.js +418 -0
- package/dist/cjs/instant-replay.js +66 -0
- package/dist/cjs/lag-compensation3d.js +837 -0
- package/dist/cjs/local-authority-server-runner.js +1121 -0
- package/dist/cjs/local-authority-wire.js +1497 -0
- package/dist/cjs/local-authority.js +817 -0
- package/dist/cjs/local-ws-adapter.js +150 -0
- package/dist/cjs/match-log.js +61 -0
- package/dist/cjs/math.js +157 -0
- package/dist/cjs/mock-network-adapter.js +26 -0
- package/dist/cjs/movement.js +703 -0
- package/dist/cjs/movement3d.js +1246 -0
- package/dist/cjs/multiclient-authority.js +308 -0
- package/dist/cjs/multiplayer-service.js +434 -0
- package/dist/cjs/navigation.js +347 -0
- package/dist/cjs/navmesh.js +1437 -0
- package/dist/cjs/networked-client.js +691 -0
- package/dist/cjs/networked-input-authority.js +443 -0
- package/dist/cjs/node.js +47 -0
- package/dist/cjs/number-lab.js +78 -0
- package/dist/cjs/offline-session.js +370 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pause-resume.js +43 -0
- package/dist/cjs/physics-cert.js +426 -0
- package/dist/cjs/physics2d.js +751 -0
- package/dist/cjs/physics3d.js +1996 -0
- package/dist/cjs/prediction-culling.js +203 -0
- package/dist/cjs/protocol-lab.js +1299 -0
- package/dist/cjs/r3f-render-adapter.js +209 -0
- package/dist/cjs/random.js +59 -0
- package/dist/cjs/replay-bundle.js +1598 -0
- package/dist/cjs/replay-inspect.js +236 -0
- package/dist/cjs/replay-scoring.js +68 -0
- package/dist/cjs/replay.js +356 -0
- package/dist/cjs/reports.js +181 -0
- package/dist/cjs/rollback-history.js +251 -0
- package/dist/cjs/runtime-guards.js +111 -0
- package/dist/cjs/sample-lab.js +145 -0
- package/dist/cjs/sample-scenes.js +752 -0
- package/dist/cjs/schema-artifacts.js +359 -0
- package/dist/cjs/schema-codegen.js +544 -0
- package/dist/cjs/schema-dsl-binary.js +655 -0
- package/dist/cjs/schema.js +319 -0
- package/dist/cjs/sdk-offline.js +99 -0
- package/dist/cjs/session-promotion.js +74 -0
- package/dist/cjs/session-snapshot.js +265 -0
- package/dist/cjs/session-wire.js +216 -0
- package/dist/cjs/signals.js +103 -0
- package/dist/cjs/sparse-set.js +169 -0
- package/dist/cjs/synctest.js +136 -0
- package/dist/cjs/system-lifecycle.js +286 -0
- package/dist/cjs/timers.js +61 -0
- package/dist/cjs/tools/static-checker.js +1468 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/venus-mp-budget.js +125 -0
- package/dist/cjs/ws-frame.js +255 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11379 -0
- package/dist/collections.d.ts +9 -0
- package/dist/collections.js +30 -0
- package/dist/collision.d.ts +25 -0
- package/dist/collision.js +85 -0
- package/dist/commands.d.ts +137 -0
- package/dist/commands.js +977 -0
- package/dist/config-bundle.d.ts +89 -0
- package/dist/config-bundle.js +483 -0
- package/dist/creator.d.ts +20 -0
- package/dist/creator.js +11 -0
- package/dist/deterministic-primitives.d.ts +59 -0
- package/dist/deterministic-primitives.js +126 -0
- package/dist/deterministic-session.d.ts +197 -0
- package/dist/deterministic-session.js +1401 -0
- package/dist/ecs-lite.d.ts +26 -0
- package/dist/ecs-lite.js +152 -0
- package/dist/effects-adapter.d.ts +21 -0
- package/dist/effects-adapter.js +94 -0
- package/dist/engine-complete.d.ts +45 -0
- package/dist/engine-complete.js +2292 -0
- package/dist/engine-core-proof.d.ts +26 -0
- package/dist/engine-core-proof.js +66 -0
- package/dist/engine-parity-matrix.d.ts +300 -0
- package/dist/engine-parity-matrix.js +869 -0
- package/dist/engine-parity.d.ts +74 -0
- package/dist/engine-parity.js +3178 -0
- package/dist/engine-runtime.d.ts +351 -0
- package/dist/engine-runtime.js +3123 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +45 -0
- package/dist/events.js +242 -0
- package/dist/examples/counter-duel.d.ts +43 -0
- package/dist/examples/counter-duel.js +150 -0
- package/dist/examples/fixed-arena.d.ts +110 -0
- package/dist/examples/fixed-arena.js +356 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +66 -0
- package/dist/input-authority.d.ts +82 -0
- package/dist/input-authority.js +147 -0
- package/dist/input-button.d.ts +36 -0
- package/dist/input-button.js +249 -0
- package/dist/input-timeline.d.ts +65 -0
- package/dist/input-timeline.js +412 -0
- package/dist/instant-replay.d.ts +59 -0
- package/dist/instant-replay.js +62 -0
- package/dist/lag-compensation3d.d.ts +146 -0
- package/dist/lag-compensation3d.js +827 -0
- package/dist/local-authority-server-runner.d.ts +79 -0
- package/dist/local-authority-server-runner.js +1118 -0
- package/dist/local-authority-wire.d.ts +142 -0
- package/dist/local-authority-wire.js +1477 -0
- package/dist/local-authority.d.ts +160 -0
- package/dist/local-authority.js +810 -0
- package/dist/local-ws-adapter.d.ts +14 -0
- package/dist/local-ws-adapter.js +146 -0
- package/dist/match-log.d.ts +74 -0
- package/dist/match-log.js +57 -0
- package/dist/math.d.ts +41 -0
- package/dist/math.js +154 -0
- package/dist/mock-network-adapter.d.ts +13 -0
- package/dist/mock-network-adapter.js +23 -0
- package/dist/movement.d.ts +155 -0
- package/dist/movement.js +694 -0
- package/dist/movement3d.d.ts +195 -0
- package/dist/movement3d.js +1238 -0
- package/dist/multiclient-authority.d.ts +30 -0
- package/dist/multiclient-authority.js +305 -0
- package/dist/multiplayer-service.d.ts +153 -0
- package/dist/multiplayer-service.js +429 -0
- package/dist/navigation.d.ts +91 -0
- package/dist/navigation.js +338 -0
- package/dist/navmesh.d.ts +236 -0
- package/dist/navmesh.js +1423 -0
- package/dist/networked-client.d.ts +124 -0
- package/dist/networked-client.js +688 -0
- package/dist/networked-input-authority.d.ts +51 -0
- package/dist/networked-input-authority.js +439 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +19 -0
- package/dist/number-lab.d.ts +15 -0
- package/dist/number-lab.js +75 -0
- package/dist/offline-session.d.ts +2 -0
- package/dist/offline-session.js +367 -0
- package/dist/pause-resume.d.ts +48 -0
- package/dist/pause-resume.js +40 -0
- package/dist/physics-cert.d.ts +40 -0
- package/dist/physics-cert.js +389 -0
- package/dist/physics2d.d.ts +142 -0
- package/dist/physics2d.js +741 -0
- package/dist/physics3d.d.ts +285 -0
- package/dist/physics3d.js +1979 -0
- package/dist/prediction-culling.d.ts +34 -0
- package/dist/prediction-culling.js +200 -0
- package/dist/protocol-lab.d.ts +115 -0
- package/dist/protocol-lab.js +1295 -0
- package/dist/r3f-render-adapter.d.ts +61 -0
- package/dist/r3f-render-adapter.js +203 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +55 -0
- package/dist/replay-bundle.d.ts +150 -0
- package/dist/replay-bundle.js +1586 -0
- package/dist/replay-inspect.d.ts +60 -0
- package/dist/replay-inspect.js +228 -0
- package/dist/replay-scoring.d.ts +28 -0
- package/dist/replay-scoring.js +65 -0
- package/dist/replay.d.ts +17 -0
- package/dist/replay.js +351 -0
- package/dist/reports.d.ts +176 -0
- package/dist/reports.js +174 -0
- package/dist/rollback-history.d.ts +46 -0
- package/dist/rollback-history.js +247 -0
- package/dist/runtime-guards.d.ts +1 -0
- package/dist/runtime-guards.js +108 -0
- package/dist/sample-lab.d.ts +32 -0
- package/dist/sample-lab.js +134 -0
- package/dist/sample-scenes.d.ts +86 -0
- package/dist/sample-scenes.js +745 -0
- package/dist/schema-artifacts.d.ts +49 -0
- package/dist/schema-artifacts.js +356 -0
- package/dist/schema-codegen.d.ts +21 -0
- package/dist/schema-codegen.js +538 -0
- package/dist/schema-dsl-binary.d.ts +75 -0
- package/dist/schema-dsl-binary.js +648 -0
- package/dist/schema.d.ts +87 -0
- package/dist/schema.js +315 -0
- package/dist/sdk-offline.d.ts +34 -0
- package/dist/sdk-offline.js +96 -0
- package/dist/session-promotion.d.ts +26 -0
- package/dist/session-promotion.js +70 -0
- package/dist/session-snapshot.d.ts +95 -0
- package/dist/session-snapshot.js +255 -0
- package/dist/session-wire.d.ts +89 -0
- package/dist/session-wire.js +211 -0
- package/dist/signals.d.ts +31 -0
- package/dist/signals.js +99 -0
- package/dist/sparse-set.d.ts +34 -0
- package/dist/sparse-set.js +165 -0
- package/dist/synctest.d.ts +31 -0
- package/dist/synctest.js +133 -0
- package/dist/system-lifecycle.d.ts +72 -0
- package/dist/system-lifecycle.js +281 -0
- package/dist/timers.d.ts +17 -0
- package/dist/timers.js +53 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/static-checker.d.ts +31 -0
- package/dist/tools/static-checker.js +1462 -0
- package/dist/tools/vite-plugin.d.ts +69 -0
- package/dist/tools/vite-plugin.js +203 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +1 -0
- package/dist/venus-mp-budget.d.ts +29 -0
- package/dist/venus-mp-budget.js +122 -0
- package/dist/ws-frame.d.ts +49 -0
- package/dist/ws-frame.js +247 -0
- package/package.json +169 -0
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkDeterminism = checkDeterminism;
|
|
4
|
+
exports.collectDeterminismSuppressions = collectDeterminismSuppressions;
|
|
5
|
+
exports.checkSourceText = checkSourceText;
|
|
6
|
+
exports.getDeterminismParserKind = getDeterminismParserKind;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_module_1 = require("node:module");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const rules = [
|
|
11
|
+
{
|
|
12
|
+
ruleId: 'determinism/no-math-random',
|
|
13
|
+
severity: 'error',
|
|
14
|
+
pattern: /\bMath\.random\s*\(/g,
|
|
15
|
+
message: 'Math.random() cannot be replayed or seeded by the deterministic session.',
|
|
16
|
+
suggestion: 'Use ctx.random.nextInt() or ctx.random.nextFixed() inside step().',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
ruleId: 'determinism/no-date-now',
|
|
20
|
+
severity: 'error',
|
|
21
|
+
pattern: /\bDate\.now\s*\(/g,
|
|
22
|
+
message: 'Date.now() uses ambient wall-clock time and diverges during replay.',
|
|
23
|
+
suggestion: 'Use ctx.frame, fixed tick counters, or deterministic scheduled events.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
ruleId: 'determinism/no-new-date',
|
|
27
|
+
severity: 'error',
|
|
28
|
+
pattern: /\bnew\s+Date\s*\(/g,
|
|
29
|
+
message: 'new Date() uses ambient wall-clock time and is not replay-safe.',
|
|
30
|
+
suggestion: 'Store deterministic frame numbers or explicit timestamps supplied as inputs.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
ruleId: 'determinism/no-performance-now',
|
|
34
|
+
severity: 'error',
|
|
35
|
+
pattern: /\bperformance\.now\s*\(/g,
|
|
36
|
+
message: 'performance.now() depends on the local runtime clock.',
|
|
37
|
+
suggestion: 'Use fixed frame counters from the deterministic step context.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
ruleId: 'determinism/no-timers',
|
|
41
|
+
severity: 'error',
|
|
42
|
+
pattern: /\b(setTimeout|setInterval|requestAnimationFrame)\s*\(/g,
|
|
43
|
+
message: 'Timers are wall-clock or render-clock driven and cannot be replayed.',
|
|
44
|
+
suggestion: 'Use countdown fields in deterministic state advanced by fixed ticks.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
ruleId: 'determinism/no-io',
|
|
48
|
+
severity: 'error',
|
|
49
|
+
pattern: /\b(fetch|WebSocket)\s*\(/g,
|
|
50
|
+
message: 'External I/O inside simulation breaks replay and rollback.',
|
|
51
|
+
suggestion: 'Move I/O outside simulation and feed results through deterministic inputs.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
ruleId: 'determinism/no-commonjs-require',
|
|
55
|
+
severity: 'error',
|
|
56
|
+
pattern: /\brequire\s*\(/g,
|
|
57
|
+
message: 'CommonJS require() can load host-dependent code paths during simulation.',
|
|
58
|
+
suggestion: 'Use static imports in non-simulation code or route external data through deterministic assets/config.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
ruleId: 'determinism/no-node-builtin-import',
|
|
62
|
+
severity: 'error',
|
|
63
|
+
pattern: /\b(?:import|export)\b[^'"]*['"](?:node:|fs|path|crypto|http|https|net|tls|child_process|worker_threads|process)['"]/g,
|
|
64
|
+
message: 'Node builtin modules expose host I/O, clocks, process state, or platform-specific behavior.',
|
|
65
|
+
suggestion: 'Keep Node-only work outside deterministic simulation and provide deterministic assets, configs, or inputs instead.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
ruleId: 'determinism/no-dynamic-code',
|
|
69
|
+
severity: 'error',
|
|
70
|
+
pattern: /\b(?:eval|Function)\s*\(/g,
|
|
71
|
+
message: 'Dynamic code evaluation bypasses static determinism checks and can vary by host runtime.',
|
|
72
|
+
suggestion: 'Use compiled deterministic systems or descriptor/codegen outputs checked into the deterministic build.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
ruleId: 'determinism/no-import-meta',
|
|
76
|
+
severity: 'error',
|
|
77
|
+
pattern: /\bimport\.meta\b/g,
|
|
78
|
+
message: 'import.meta exposes host module identity and environment-specific metadata.',
|
|
79
|
+
suggestion: 'Resolve module metadata outside simulation and pass stable values through deterministic config assets.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
ruleId: 'determinism/no-process-env',
|
|
83
|
+
severity: 'error',
|
|
84
|
+
pattern: /\bprocess\.env\b/g,
|
|
85
|
+
message: 'process.env reads depend on host configuration and are not replay-safe.',
|
|
86
|
+
suggestion: 'Resolve environment configuration before simulation and pass it through deterministic config assets.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
ruleId: 'determinism/no-locale-compare',
|
|
90
|
+
severity: 'error',
|
|
91
|
+
pattern: /\.localeCompare\s*\(/g,
|
|
92
|
+
message: 'localeCompare can vary by runtime locale and ICU data.',
|
|
93
|
+
suggestion: 'Use ordinal comparison: left < right ? -1 : left > right ? 1 : 0.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
ruleId: 'determinism/no-intl-locale',
|
|
97
|
+
severity: 'error',
|
|
98
|
+
pattern: /\bIntl\.(Collator|DateTimeFormat|NumberFormat|PluralRules|RelativeTimeFormat|ListFormat|Segmenter|DisplayNames)\s*\(/g,
|
|
99
|
+
message: 'Intl formatting and collation depend on runtime ICU data and locale support.',
|
|
100
|
+
suggestion: 'Precompute locale-sensitive data outside simulation or use deterministic ordinal/quantized formatting tables.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
ruleId: 'determinism/no-locale-format',
|
|
104
|
+
severity: 'error',
|
|
105
|
+
pattern: /\.(toLocaleString|toLocaleDateString|toLocaleTimeString|toLocaleLowerCase|toLocaleUpperCase)\s*\(/g,
|
|
106
|
+
message: 'Locale-sensitive formatting can vary by runtime locale and ICU data.',
|
|
107
|
+
suggestion: 'Format outside simulation or use deterministic fixed-format serializers.',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
ruleId: 'determinism/no-float-format',
|
|
111
|
+
severity: 'error',
|
|
112
|
+
pattern: /(?<!math)\.(toFixed|toPrecision)\s*\(/g,
|
|
113
|
+
message: 'toFixed/toPrecision rounding has diverged across JS engines (Hermes vs V8) — a cross-engine desync vector when the result feeds checksummed state.',
|
|
114
|
+
suggestion: 'Quantize with pure integer arithmetic (Math.round(value * scale) / scale) or the fixed-point math helpers.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
ruleId: 'determinism/no-crypto-random',
|
|
118
|
+
severity: 'error',
|
|
119
|
+
pattern: /\bcrypto\.(getRandomValues|randomUUID)\s*\(/g,
|
|
120
|
+
message: 'Crypto random APIs use external entropy.',
|
|
121
|
+
suggestion: 'Use deterministic session RNG seeded by replay identity.',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
ruleId: 'determinism/no-object-key-order',
|
|
125
|
+
severity: 'warn',
|
|
126
|
+
pattern: /\bObject\.(keys|values|entries)\s*\(/g,
|
|
127
|
+
message: 'Object key iteration can hide ordering assumptions in simulation code.',
|
|
128
|
+
suggestion: 'Sort keys explicitly or use schema-declared arrays with stable indexes.',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
ruleId: 'determinism/no-for-in',
|
|
132
|
+
severity: 'warn',
|
|
133
|
+
pattern: /\bfor\s*\([^)]*\bin\b[^)]*\)/g,
|
|
134
|
+
message: 'for...in iteration can hide ordering assumptions and prototype pollution.',
|
|
135
|
+
suggestion: 'Iterate schema-declared arrays or Object.keys(record).sort() with an explicit comparator.',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
ruleId: 'determinism/no-reflect-own-keys',
|
|
139
|
+
severity: 'error',
|
|
140
|
+
pattern: /\bReflect\.ownKeys\s*\(/g,
|
|
141
|
+
message: 'Reflect.ownKeys() exposes host object key ordering assumptions.',
|
|
142
|
+
suggestion: 'Use schema-declared keys or Object.keys(record).sort(compareStableStrings).',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
ruleId: 'determinism/no-native-map-set-iteration',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
pattern: /\b(?:Array\.from\s*\(\s*[A-Za-z_$][\w$]*\s*\)|\[\s*\.\.\.\s*[A-Za-z_$][\w$]*\s*\])/g,
|
|
148
|
+
message: 'Native Map/Set iteration order can hide nondeterministic insertion-order assumptions.',
|
|
149
|
+
suggestion: 'Use deterministic frame collections or materialize entries sorted by stable schema ids.',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
ruleId: 'determinism/no-sort-without-comparator',
|
|
153
|
+
severity: 'error',
|
|
154
|
+
pattern: /\.sort\s*\(\s*\)/g,
|
|
155
|
+
message: 'Array.sort() without a comparator relies on implicit string conversion.',
|
|
156
|
+
suggestion: 'Pass an explicit deterministic comparator.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
ruleId: 'determinism/no-random-sort-comparator',
|
|
160
|
+
severity: 'error',
|
|
161
|
+
pattern: /\.sort\s*\(.*\bMath\.random\s*\(/g,
|
|
162
|
+
message: 'Random sort comparators produce unstable ordering and cannot be replayed.',
|
|
163
|
+
suggestion: 'Use a deterministic shuffle driven by ctx.random or sort by stable ids.',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
ruleId: 'determinism/no-extracted-mutating-method',
|
|
167
|
+
severity: 'error',
|
|
168
|
+
pattern: /\b(?:push|pop|shift|unshift|splice|sort|reverse|fill|copyWithin|set|add|delete|clear)\.(?:call|apply|bind)\s*\(/g,
|
|
169
|
+
message: 'Extracted mutating collection methods hide deterministic state mutation from the checker.',
|
|
170
|
+
suggestion: 'Mutate only rollback-owned state through deterministic APIs or use local scratch collections inside a single step.',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
ruleId: 'determinism/no-async-step',
|
|
174
|
+
// error, not warn: an async step() (or promise scheduling inside the sim)
|
|
175
|
+
// breaks frame atomicity outright — never advisory.
|
|
176
|
+
severity: 'error',
|
|
177
|
+
pattern: /\basync\s+function\b|\bnew\s+Promise\s*\(|\bPromise(?:\.[A-Za-z_$][\w$]*)?\s*\(|\b(queueMicrotask|setImmediate)\s*\(/g,
|
|
178
|
+
message: 'Async scheduling is not deterministic inside simulation.',
|
|
179
|
+
suggestion: 'Keep simulation steps synchronous and model effects as explicit outputs.',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
ruleId: 'determinism/no-dynamic-import',
|
|
183
|
+
severity: 'error',
|
|
184
|
+
pattern: /\bimport\s*\(/g,
|
|
185
|
+
message: 'Dynamic import() can load host-dependent code paths during simulation.',
|
|
186
|
+
suggestion: 'Use top-level static imports outside deterministic simulation or route external data through deterministic assets/config.',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
ruleId: 'determinism/no-trig-math',
|
|
190
|
+
// error, not warn: libm-divergent float math is the desync class this
|
|
191
|
+
// engine exists to prevent — certification must block it out of the box.
|
|
192
|
+
severity: 'error',
|
|
193
|
+
pattern: /\bMath\.(sin|cos|tan|asin|acos|atan|atan2|sqrt|cbrt|hypot|pow|log|log10|log2|log1p|exp|expm1|fround|sinh|cosh|tanh|asinh|acosh|atanh)\s*\(/g,
|
|
194
|
+
message: 'Native floating math can drift across runtimes when checksummed.',
|
|
195
|
+
suggestion: 'Use deterministic math wrappers, fixed-point helpers, or quantized lookup tables.',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
ruleId: 'determinism/no-render-object-mutation',
|
|
199
|
+
severity: 'error',
|
|
200
|
+
pattern: /\b(scene|mesh|camera|group|object3D)\.(position|rotation|scale)\b/g,
|
|
201
|
+
message: 'Render object mutation belongs outside checksummed simulation state.',
|
|
202
|
+
suggestion: 'Mutate plain deterministic state in step(), then map frames to r3f/Three props in render code.',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
ruleId: 'determinism/no-uncertified-physics-step',
|
|
206
|
+
severity: 'error',
|
|
207
|
+
pattern: /\b(world|physicsWorld|rapierWorld|matterWorld)\.step\s*\(/g,
|
|
208
|
+
message: 'Physics engine stepping inside simulation is blocked until that engine passes rollback certification.',
|
|
209
|
+
suggestion: 'Use fixed-point collision helpers or a certified physics adapter with snapshot/restore checks.',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
ruleId: 'determinism/no-module-mutable-state',
|
|
213
|
+
severity: 'error',
|
|
214
|
+
pattern: /^\s*(?:export\s+)?let\s+[A-Za-z_$][\w$]*/gm,
|
|
215
|
+
message: 'Mutable module state survives rollback and violates stateless deterministic system rules.',
|
|
216
|
+
suggestion: 'Move mutable values into checksummed frame state, singleton components, or system-local scratch created inside the step.',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
ruleId: 'determinism/no-closure-mutable-state',
|
|
220
|
+
severity: 'error',
|
|
221
|
+
pattern: /\b[A-Za-z_$][\w$]*\s*(?:\+\+|--|[+\-*/%]?=)/g,
|
|
222
|
+
message: 'Closure mutable state can persist across rollback outside the deterministic frame.',
|
|
223
|
+
suggestion: 'Store counters and accumulators in frame state or local variables recreated each step.',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
ruleId: 'determinism/no-mutable-class-state',
|
|
227
|
+
severity: 'error',
|
|
228
|
+
pattern: /\bclass\s+[A-Za-z_$][\w$]*[\s\S]*?\{[\s\S]*?\b[A-Za-z_$][\w$]*\s*(?:=|;)/g,
|
|
229
|
+
message: 'Class fields create mutable simulation object state outside the deterministic frame.',
|
|
230
|
+
suggestion: 'Represent deterministic data as plain frame components and keep systems stateless.',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
ruleId: 'determinism/no-mutable-collection-state',
|
|
234
|
+
severity: 'error',
|
|
235
|
+
pattern: /^\s*(?:export\s+)?const\s+[A-Za-z_$][\w$]*\s*=\s*new\s+(Map|Set|WeakMap|WeakSet)\s*\(/gm,
|
|
236
|
+
message: 'Native mutable collections preserve insertion and mutation state outside declared deterministic storage.',
|
|
237
|
+
suggestion: 'Use deterministic frame collections or recreate local collections inside a step and serialize their ordered contents.',
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
const requireModule = (0, node_module_1.createRequire)((0, node_path_1.resolve)('package.json'));
|
|
241
|
+
let parserKind;
|
|
242
|
+
const unsafeNativeMathExpressions = new Set([
|
|
243
|
+
'Math.sin', 'Math.cos', 'Math.tan',
|
|
244
|
+
'Math.asin', 'Math.acos', 'Math.atan', 'Math.atan2',
|
|
245
|
+
'Math.sqrt', 'Math.cbrt', 'Math.hypot', 'Math.pow',
|
|
246
|
+
'Math.log', 'Math.log10', 'Math.log2', 'Math.log1p',
|
|
247
|
+
'Math.exp', 'Math.expm1', 'Math.fround',
|
|
248
|
+
'Math.sinh', 'Math.cosh', 'Math.tanh',
|
|
249
|
+
'Math.asinh', 'Math.acosh', 'Math.atanh',
|
|
250
|
+
]);
|
|
251
|
+
const nodeBuiltinSpecifiers = new Set([
|
|
252
|
+
'assert',
|
|
253
|
+
'async_hooks',
|
|
254
|
+
'buffer',
|
|
255
|
+
'child_process',
|
|
256
|
+
'cluster',
|
|
257
|
+
'crypto',
|
|
258
|
+
'dgram',
|
|
259
|
+
'diagnostics_channel',
|
|
260
|
+
'dns',
|
|
261
|
+
'events',
|
|
262
|
+
'fs',
|
|
263
|
+
'http',
|
|
264
|
+
'http2',
|
|
265
|
+
'https',
|
|
266
|
+
'inspector',
|
|
267
|
+
'module',
|
|
268
|
+
'net',
|
|
269
|
+
'os',
|
|
270
|
+
'path',
|
|
271
|
+
'perf_hooks',
|
|
272
|
+
'process',
|
|
273
|
+
'readline',
|
|
274
|
+
'stream',
|
|
275
|
+
'string_decoder',
|
|
276
|
+
'timers',
|
|
277
|
+
'tls',
|
|
278
|
+
'tty',
|
|
279
|
+
'url',
|
|
280
|
+
'util',
|
|
281
|
+
'v8',
|
|
282
|
+
'vm',
|
|
283
|
+
'worker_threads',
|
|
284
|
+
'zlib',
|
|
285
|
+
]);
|
|
286
|
+
async function checkDeterminism(options) {
|
|
287
|
+
const mode = options.mode ?? 'warn';
|
|
288
|
+
const files = await collectFiles(options.paths, options.followImports ?? true, options.project);
|
|
289
|
+
const diagnostics = [];
|
|
290
|
+
const schemaOptions = createSchemaOptions(options.schemaStateFields, options.stateObjectName);
|
|
291
|
+
for (const file of files) {
|
|
292
|
+
const content = await (0, promises_1.readFile)(file, 'utf8');
|
|
293
|
+
diagnostics.push(...checkFile(file, content, mode, schemaOptions, options.parserKind));
|
|
294
|
+
}
|
|
295
|
+
return diagnostics;
|
|
296
|
+
}
|
|
297
|
+
async function collectDeterminismSuppressions(paths) {
|
|
298
|
+
const files = await collectFiles(paths, true);
|
|
299
|
+
const suppressions = [];
|
|
300
|
+
for (const file of files) {
|
|
301
|
+
const lines = (await (0, promises_1.readFile)(file, 'utf8')).split(/\r?\n/);
|
|
302
|
+
lines.forEach((line, index) => {
|
|
303
|
+
const suppression = parseSuppressionDetails(line);
|
|
304
|
+
if (suppression !== undefined) {
|
|
305
|
+
suppressions.push({
|
|
306
|
+
path: file,
|
|
307
|
+
line: index + 1,
|
|
308
|
+
...suppression,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/* istanbul ignore next -- one suppression can exist per source line, so the rule-id tie-breaker is defensive ordering. */
|
|
314
|
+
return suppressions.sort((left, right) => left.path.localeCompare(right.path) || left.line - right.line || left.ruleId.localeCompare(right.ruleId));
|
|
315
|
+
}
|
|
316
|
+
function checkSourceText(path, content, mode = 'warn', options = {}) {
|
|
317
|
+
return checkFile(path, content, mode, createSchemaOptions(options.schemaStateFields, options.stateObjectName), options.parserKind);
|
|
318
|
+
}
|
|
319
|
+
function getDeterminismParserKind() {
|
|
320
|
+
if (parserKind !== undefined) {
|
|
321
|
+
return parserKind;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
requireModule.resolve('typescript');
|
|
325
|
+
parserKind = 'typescript-ast';
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
/* istanbul ignore next -- parserKind override covers regex behavior without requiring a no-typescript install fixture. */
|
|
329
|
+
parserKind = 'regex-scanner';
|
|
330
|
+
}
|
|
331
|
+
return parserKind;
|
|
332
|
+
}
|
|
333
|
+
function checkFile(path, content, mode, schemaOptions, parserKindOverride) {
|
|
334
|
+
if ((parserKindOverride ?? getDeterminismParserKind()) === 'typescript-ast') {
|
|
335
|
+
return checkFileWithAst(path, content, mode, schemaOptions);
|
|
336
|
+
}
|
|
337
|
+
return checkFileWithRegex(path, content, mode, schemaOptions);
|
|
338
|
+
}
|
|
339
|
+
function checkFileWithAst(path, content, mode, schemaOptions) {
|
|
340
|
+
const typescript = requireModule('typescript');
|
|
341
|
+
const sourceFile = typescript.createSourceFile(path, content, typescript.ScriptTarget.Latest, true, typescript.ScriptKind.TS);
|
|
342
|
+
const diagnostics = [];
|
|
343
|
+
const declarations = new Map();
|
|
344
|
+
const forbiddenAliases = new Map();
|
|
345
|
+
const namespaceAliases = new Map();
|
|
346
|
+
const mutatingMethodAliases = new Set();
|
|
347
|
+
const functionStack = [];
|
|
348
|
+
let functionDepth = 0;
|
|
349
|
+
let returnedFunctionDepth = 0;
|
|
350
|
+
diagnostics.push(...parseDiagnosticsToDeterminismDiagnostics(typescript, sourceFile, path));
|
|
351
|
+
if (diagnostics.some((diagnostic) => diagnostic.ruleId === 'determinism/syntax-error')) {
|
|
352
|
+
return diagnostics;
|
|
353
|
+
}
|
|
354
|
+
visit(sourceFile);
|
|
355
|
+
return diagnostics.sort((left, right) => left.line - right.line || left.column - right.column || left.ruleId.localeCompare(right.ruleId));
|
|
356
|
+
function visit(node) {
|
|
357
|
+
if (isFunctionScopeNode(node)) {
|
|
358
|
+
functionDepth += 1;
|
|
359
|
+
functionStack.push(functionName(node));
|
|
360
|
+
const returnedFunction = isReturnedFunction(node);
|
|
361
|
+
returnedFunctionDepth += returnedFunction ? 1 : 0;
|
|
362
|
+
checkFunctionNode(node);
|
|
363
|
+
typescript.forEachChild(node, visit);
|
|
364
|
+
returnedFunctionDepth -= returnedFunction ? 1 : 0;
|
|
365
|
+
functionStack.pop();
|
|
366
|
+
functionDepth -= 1;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (typescript.isVariableStatement(node)) {
|
|
370
|
+
checkVariableStatement(node);
|
|
371
|
+
}
|
|
372
|
+
if (typescript.isVariableDeclaration(node)) {
|
|
373
|
+
registerVariableDeclaration(node);
|
|
374
|
+
}
|
|
375
|
+
if (typescript.isCallExpression(node)) {
|
|
376
|
+
checkCallExpression(node);
|
|
377
|
+
}
|
|
378
|
+
if (typescript.isImportDeclaration(node) || typescript.isExportDeclaration(node)) {
|
|
379
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
380
|
+
if (moduleSpecifier !== undefined && typescript.isStringLiteralLike(moduleSpecifier) && isNodeBuiltinSpecifier(moduleSpecifier.text)) {
|
|
381
|
+
pushRule(node, 'determinism/no-node-builtin-import');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (typescript.isImportEqualsDeclaration(node) && typescript.isExternalModuleReference(node.moduleReference)) {
|
|
385
|
+
const expression = node.moduleReference.expression;
|
|
386
|
+
if (typescript.isStringLiteralLike(expression) && isNodeBuiltinSpecifier(expression.text)) {
|
|
387
|
+
pushRule(node, 'determinism/no-node-builtin-import');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (typescript.isNewExpression(node)) {
|
|
391
|
+
const expressionText = normalizedExpressionText(node.expression);
|
|
392
|
+
if (expressionText === 'Date') {
|
|
393
|
+
pushRule(node, 'determinism/no-new-date');
|
|
394
|
+
}
|
|
395
|
+
if (expressionText === 'Function' || expressionText === 'globalThis.Function') {
|
|
396
|
+
pushRule(node, 'determinism/no-dynamic-code');
|
|
397
|
+
}
|
|
398
|
+
if (expressionText === 'Promise' || expressionText === 'globalThis.Promise') {
|
|
399
|
+
pushRule(node, 'determinism/no-async-step');
|
|
400
|
+
}
|
|
401
|
+
if (expressionText.startsWith('Intl.') || expressionText.startsWith('globalThis.Intl.')) {
|
|
402
|
+
pushRule(node, 'determinism/no-intl-locale');
|
|
403
|
+
}
|
|
404
|
+
if (typescript.isIdentifier(node.expression)) {
|
|
405
|
+
const aliasRule = forbiddenAliases.get(node.expression.text);
|
|
406
|
+
if (aliasRule === 'determinism/no-intl-locale' || aliasRule === 'determinism/no-dynamic-code') {
|
|
407
|
+
pushRule(node, aliasRule);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (typescript.isNewExpression(node) &&
|
|
412
|
+
(normalizedExpressionText(node.expression) === 'WebSocket' || normalizedExpressionText(node.expression) === 'globalThis.WebSocket')) {
|
|
413
|
+
pushRule(node, 'determinism/no-io');
|
|
414
|
+
}
|
|
415
|
+
if (typescript.isForInStatement(node)) {
|
|
416
|
+
pushRule(node, 'determinism/no-for-in');
|
|
417
|
+
}
|
|
418
|
+
if (node.kind === typescript.SyntaxKind.MetaProperty && node.getText(sourceFile) === 'import.meta') {
|
|
419
|
+
pushRule(node, 'determinism/no-import-meta');
|
|
420
|
+
}
|
|
421
|
+
if (typescript.isForOfStatement(node)) {
|
|
422
|
+
checkNativeCollectionIteration(node.expression, node);
|
|
423
|
+
}
|
|
424
|
+
if (typescript.isSpreadElement(node)) {
|
|
425
|
+
checkNativeCollectionIteration(node.expression, node);
|
|
426
|
+
}
|
|
427
|
+
if (typescript.isPropertyAccessExpression(node) &&
|
|
428
|
+
isRenderObjectReceiver(node.expression.getText(sourceFile)) &&
|
|
429
|
+
(node.name.text === 'position' || node.name.text === 'rotation' || node.name.text === 'scale')) {
|
|
430
|
+
pushRule(node, 'determinism/no-render-object-mutation');
|
|
431
|
+
}
|
|
432
|
+
if (typescript.isPropertyAccessExpression(node) &&
|
|
433
|
+
normalizedExpressionText(node) === 'process.env') {
|
|
434
|
+
pushRule(node, 'determinism/no-process-env');
|
|
435
|
+
}
|
|
436
|
+
if (typescript.isElementAccessExpression(node) &&
|
|
437
|
+
normalizedExpressionText(node) === 'process.env') {
|
|
438
|
+
pushRule(node, 'determinism/no-process-env');
|
|
439
|
+
}
|
|
440
|
+
if (typescript.isPropertyDeclaration(node) && isSystemLikeClassProperty(node)) {
|
|
441
|
+
pushRule(node, 'determinism/no-mutable-class-state');
|
|
442
|
+
}
|
|
443
|
+
if (typescript.isBinaryExpression(node) &&
|
|
444
|
+
isMutationOperator(node.operatorToken.kind) &&
|
|
445
|
+
typescript.isIdentifier(node.left)) {
|
|
446
|
+
checkMutableIdentifierWrite(node.left);
|
|
447
|
+
}
|
|
448
|
+
if (typescript.isBinaryExpression(node) &&
|
|
449
|
+
isMutationOperator(node.operatorToken.kind) &&
|
|
450
|
+
(typescript.isPropertyAccessExpression(node.left) || typescript.isElementAccessExpression(node.left))) {
|
|
451
|
+
checkCapturedMutableValueUse(node.left);
|
|
452
|
+
}
|
|
453
|
+
if ((typescript.isPrefixUnaryExpression(node) || typescript.isPostfixUnaryExpression(node)) &&
|
|
454
|
+
(node.operator === typescript.SyntaxKind.PlusPlusToken || node.operator === typescript.SyntaxKind.MinusMinusToken) &&
|
|
455
|
+
typescript.isIdentifier(node.operand)) {
|
|
456
|
+
checkMutableIdentifierWrite(node.operand);
|
|
457
|
+
}
|
|
458
|
+
if ((typescript.isPrefixUnaryExpression(node) || typescript.isPostfixUnaryExpression(node)) &&
|
|
459
|
+
(node.operator === typescript.SyntaxKind.PlusPlusToken || node.operator === typescript.SyntaxKind.MinusMinusToken) &&
|
|
460
|
+
(typescript.isPropertyAccessExpression(node.operand) || typescript.isElementAccessExpression(node.operand))) {
|
|
461
|
+
checkCapturedMutableValueUse(node.operand);
|
|
462
|
+
}
|
|
463
|
+
if (schemaOptions.stateFields !== undefined) {
|
|
464
|
+
checkStateFieldAccess(node, schemaOptions.stateFields);
|
|
465
|
+
}
|
|
466
|
+
typescript.forEachChild(node, visit);
|
|
467
|
+
}
|
|
468
|
+
function isFunctionScopeNode(node) {
|
|
469
|
+
return typescript.isFunctionDeclaration(node)
|
|
470
|
+
|| typescript.isFunctionExpression(node)
|
|
471
|
+
|| typescript.isArrowFunction(node)
|
|
472
|
+
|| typescript.isMethodDeclaration(node);
|
|
473
|
+
}
|
|
474
|
+
function checkFunctionNode(node) {
|
|
475
|
+
if (node.modifiers?.some((modifier) => modifier.kind === typescript.SyntaxKind.AsyncKeyword) === true) {
|
|
476
|
+
pushRule(node, 'determinism/no-async-step');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function checkVariableStatement(node) {
|
|
480
|
+
if ((node.declarationList.flags & typescript.NodeFlags.Let) !== 0 && node.parent === sourceFile) {
|
|
481
|
+
pushRule(node, 'determinism/no-module-mutable-state');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function registerVariableDeclaration(node) {
|
|
485
|
+
registerForbiddenAlias(node);
|
|
486
|
+
if (!typescript.isIdentifier(node.name)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const mutable = (node.parent.flags & typescript.NodeFlags.Let) !== 0;
|
|
490
|
+
declarations.set(node.name.text, {
|
|
491
|
+
functionDepth,
|
|
492
|
+
mutable,
|
|
493
|
+
capturedMutableValue: isCapturedMutableValueInitializer(node.initializer),
|
|
494
|
+
nativeCollectionKind: nativeCollectionInitializerKind(node.initializer),
|
|
495
|
+
});
|
|
496
|
+
if (functionDepth === 0 && node.initializer !== undefined && isMutableCollectionExpression(node.initializer)) {
|
|
497
|
+
pushRule(node.initializer, 'determinism/no-mutable-collection-state');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function registerForbiddenAlias(node) {
|
|
501
|
+
if (node.initializer === undefined) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (typescript.isIdentifier(node.name)) {
|
|
505
|
+
const expressionText = normalizedExpressionText(node.initializer);
|
|
506
|
+
const namespaceRoot = forbiddenNamespaceRoot(expressionText);
|
|
507
|
+
if (namespaceRoot !== undefined) {
|
|
508
|
+
namespaceAliases.set(node.name.text, namespaceRoot);
|
|
509
|
+
}
|
|
510
|
+
const rule = forbiddenExpressionRule(expressionText);
|
|
511
|
+
if (rule !== undefined) {
|
|
512
|
+
forbiddenAliases.set(node.name.text, rule);
|
|
513
|
+
}
|
|
514
|
+
if (isExtractedMutatingMethodExpression(expressionText)) {
|
|
515
|
+
mutatingMethodAliases.add(node.name.text);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (typescript.isArrayBindingPattern(node.name)) {
|
|
520
|
+
registerArrayBindingAliases(node);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const objectBinding = node.name;
|
|
524
|
+
const objectText = normalizedExpressionText(node.initializer);
|
|
525
|
+
for (const element of objectBinding.elements) {
|
|
526
|
+
if (!typescript.isIdentifier(element.name)) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const propertyName = element.propertyName?.getText(sourceFile) ?? element.name.text;
|
|
530
|
+
const rule = forbiddenExpressionRule(`${objectText}.${propertyName}`);
|
|
531
|
+
if (rule !== undefined) {
|
|
532
|
+
forbiddenAliases.set(element.name.text, rule);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function registerArrayBindingAliases(node) {
|
|
537
|
+
const initializer = node.initializer;
|
|
538
|
+
if (initializer === undefined || !typescript.isArrayBindingPattern(node.name) || !typescript.isArrayLiteralExpression(initializer)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
node.name.elements.forEach((element, index) => {
|
|
542
|
+
if (!typescript.isBindingElement(element) || !typescript.isIdentifier(element.name)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const initializerElement = initializer.elements[index];
|
|
546
|
+
if (initializerElement === undefined || typescript.isSpreadElement(initializerElement)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const expressionText = normalizedExpressionText(initializerElement);
|
|
550
|
+
const rule = forbiddenExpressionRule(expressionText);
|
|
551
|
+
if (rule !== undefined) {
|
|
552
|
+
forbiddenAliases.set(element.name.text, rule);
|
|
553
|
+
}
|
|
554
|
+
if (isExtractedMutatingMethodExpression(expressionText)) {
|
|
555
|
+
mutatingMethodAliases.add(element.name.text);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function checkMutableIdentifierWrite(node) {
|
|
560
|
+
const declaration = declarations.get(node.text);
|
|
561
|
+
if (declaration === undefined || !declaration.mutable || declaration.functionDepth === functionDepth) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (declaration.functionDepth > 0 && returnedFunctionDepth === 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
pushRule(node, 'determinism/no-closure-mutable-state');
|
|
568
|
+
}
|
|
569
|
+
function checkCapturedMutableValueUse(node) {
|
|
570
|
+
const root = rootIdentifier(node.expression);
|
|
571
|
+
if (root === undefined || !isCapturedMutableValueMutation(root)) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
pushRule(node, 'determinism/no-closure-mutable-state');
|
|
575
|
+
}
|
|
576
|
+
function checkCapturedMutatingMethodCall(node) {
|
|
577
|
+
if (!typescript.isPropertyAccessExpression(node.expression)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const methodName = node.expression.name.text;
|
|
581
|
+
if (!isMutatingMethodName(methodName)) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
const root = rootIdentifier(node.expression.expression);
|
|
585
|
+
if (root === undefined || !isCapturedMutableValueMutation(root)) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
pushRule(node.expression, 'determinism/no-closure-mutable-state');
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
function checkNativeCollectionIteration(expression, node) {
|
|
592
|
+
if (!isDeterministicStepScope()) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const collectionKind = nativeCollectionExpressionKind(expression);
|
|
596
|
+
if (collectionKind === 'Map' || collectionKind === 'Set') {
|
|
597
|
+
pushRule(node, 'determinism/no-native-map-set-iteration');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function isCapturedMutableValueMutation(identifier) {
|
|
601
|
+
const declaration = declarations.get(identifier.text);
|
|
602
|
+
if (declaration === undefined || !declaration.capturedMutableValue || declaration.functionDepth === functionDepth) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
if (!isDeterministicStepScope()) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
if (declaration.functionDepth > 0 && returnedFunctionDepth === 0) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
function isDeterministicStepScope() {
|
|
614
|
+
return functionStack.some((name) => name === 'step' || name === 'simulate' || name === 'mutate' || name === 'update');
|
|
615
|
+
}
|
|
616
|
+
function isReturnedFunction(node) {
|
|
617
|
+
if (typescript.isMethodDeclaration(node)) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
let parent = node.parent;
|
|
621
|
+
while (parent !== undefined && typescript.isParenthesizedExpression(parent)) {
|
|
622
|
+
parent = parent.parent;
|
|
623
|
+
}
|
|
624
|
+
return parent !== undefined && typescript.isReturnStatement(parent);
|
|
625
|
+
}
|
|
626
|
+
function isSystemLikeClassProperty(node) {
|
|
627
|
+
let parent = node.parent;
|
|
628
|
+
/* istanbul ignore next -- TypeScript PropertyDeclaration nodes are class-owned; this loop is defensive for malformed ASTs. */
|
|
629
|
+
while (parent !== undefined && !typescript.isClassDeclaration(parent)) {
|
|
630
|
+
parent = parent.parent;
|
|
631
|
+
}
|
|
632
|
+
/* istanbul ignore next -- TypeScript PropertyDeclaration nodes are class-owned; retained as a defensive malformed-AST guard. */
|
|
633
|
+
if (parent === undefined || !typescript.isClassDeclaration(parent)) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
const className = parent.name?.text ?? '';
|
|
637
|
+
return /(?:System|Processor|Simulation|Runtime|Stepper|Controller)$/.test(className);
|
|
638
|
+
}
|
|
639
|
+
function isMutationOperator(kind) {
|
|
640
|
+
return kind === typescript.SyntaxKind.EqualsToken
|
|
641
|
+
|| kind === typescript.SyntaxKind.PlusEqualsToken
|
|
642
|
+
|| kind === typescript.SyntaxKind.MinusEqualsToken
|
|
643
|
+
|| kind === typescript.SyntaxKind.AsteriskEqualsToken
|
|
644
|
+
|| kind === typescript.SyntaxKind.SlashEqualsToken
|
|
645
|
+
|| kind === typescript.SyntaxKind.PercentEqualsToken;
|
|
646
|
+
}
|
|
647
|
+
function isMutableCollectionExpression(node) {
|
|
648
|
+
return nativeCollectionInitializerKind(node) !== undefined;
|
|
649
|
+
}
|
|
650
|
+
function nativeCollectionInitializerKind(node) {
|
|
651
|
+
if (node === undefined || !typescript.isNewExpression(node)) {
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
const expressionText = normalizedExpressionText(node.expression);
|
|
655
|
+
if (expressionText === 'Map' || expressionText === 'globalThis.Map') {
|
|
656
|
+
return 'Map';
|
|
657
|
+
}
|
|
658
|
+
if (expressionText === 'Set' || expressionText === 'globalThis.Set') {
|
|
659
|
+
return 'Set';
|
|
660
|
+
}
|
|
661
|
+
if (expressionText === 'WeakMap' || expressionText === 'globalThis.WeakMap') {
|
|
662
|
+
return 'WeakMap';
|
|
663
|
+
}
|
|
664
|
+
if (expressionText === 'WeakSet' || expressionText === 'globalThis.WeakSet') {
|
|
665
|
+
return 'WeakSet';
|
|
666
|
+
}
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
function nativeCollectionExpressionKind(node) {
|
|
670
|
+
const initializerKind = nativeCollectionInitializerKind(node);
|
|
671
|
+
if (initializerKind !== undefined) {
|
|
672
|
+
return initializerKind;
|
|
673
|
+
}
|
|
674
|
+
const root = rootIdentifier(node);
|
|
675
|
+
return root === undefined ? undefined : declarations.get(root.text)?.nativeCollectionKind;
|
|
676
|
+
}
|
|
677
|
+
function isCapturedMutableValueInitializer(node) {
|
|
678
|
+
if (node === undefined) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
return isMutableCollectionExpression(node)
|
|
682
|
+
|| typescript.isObjectLiteralExpression(node)
|
|
683
|
+
|| typescript.isArrayLiteralExpression(node);
|
|
684
|
+
}
|
|
685
|
+
function rootIdentifier(node) {
|
|
686
|
+
let current = node;
|
|
687
|
+
while (typescript.isPropertyAccessExpression(current) || typescript.isElementAccessExpression(current)) {
|
|
688
|
+
current = current.expression;
|
|
689
|
+
}
|
|
690
|
+
return typescript.isIdentifier(current) ? current : undefined;
|
|
691
|
+
}
|
|
692
|
+
function isMutatingMethodName(methodName) {
|
|
693
|
+
return methodName === 'push'
|
|
694
|
+
|| methodName === 'pop'
|
|
695
|
+
|| methodName === 'shift'
|
|
696
|
+
|| methodName === 'unshift'
|
|
697
|
+
|| methodName === 'splice'
|
|
698
|
+
|| methodName === 'sort'
|
|
699
|
+
|| methodName === 'reverse'
|
|
700
|
+
|| methodName === 'fill'
|
|
701
|
+
|| methodName === 'copyWithin'
|
|
702
|
+
|| methodName === 'set'
|
|
703
|
+
|| methodName === 'add'
|
|
704
|
+
|| methodName === 'delete'
|
|
705
|
+
|| methodName === 'clear';
|
|
706
|
+
}
|
|
707
|
+
function isExtractedMutatingMethodExpression(expressionText) {
|
|
708
|
+
const methodName = expressionText.split('.').at(-1);
|
|
709
|
+
return methodName !== undefined && isMutatingMethodName(methodName);
|
|
710
|
+
}
|
|
711
|
+
function checkCallExpression(node) {
|
|
712
|
+
if (checkCapturedMutatingMethodCall(node)) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (node.expression.kind === typescript.SyntaxKind.ImportKeyword) {
|
|
716
|
+
pushRule(node, 'determinism/no-dynamic-import');
|
|
717
|
+
const moduleSpecifier = node.arguments[0];
|
|
718
|
+
if (moduleSpecifier !== undefined && typescript.isStringLiteralLike(moduleSpecifier) && isNodeBuiltinSpecifier(moduleSpecifier.text)) {
|
|
719
|
+
pushRule(node, 'determinism/no-node-builtin-import');
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const expressionText = normalizedExpressionText(node.expression);
|
|
724
|
+
if (isAllowedCertificationTimerCall(expressionText)) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (typescript.isIdentifier(node.expression)) {
|
|
728
|
+
const aliasRule = forbiddenAliases.get(node.expression.text);
|
|
729
|
+
if (aliasRule !== undefined) {
|
|
730
|
+
pushRule(node, aliasRule);
|
|
731
|
+
if (aliasRule === 'determinism/no-commonjs-require') {
|
|
732
|
+
const moduleSpecifier = node.arguments[0];
|
|
733
|
+
if (moduleSpecifier !== undefined && typescript.isStringLiteralLike(moduleSpecifier) && isNodeBuiltinSpecifier(moduleSpecifier.text)) {
|
|
734
|
+
pushRule(node, 'determinism/no-node-builtin-import');
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (mutatingMethodAliases.has(node.expression.text)) {
|
|
740
|
+
pushRule(node, 'determinism/no-extracted-mutating-method');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const indirectRule = indirectForbiddenCallRule(node);
|
|
745
|
+
if (indirectRule !== undefined) {
|
|
746
|
+
pushRule(node, indirectRule);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (expressionText === 'require' || expressionText === 'globalThis.require') {
|
|
750
|
+
pushRule(node, 'determinism/no-commonjs-require');
|
|
751
|
+
const moduleSpecifier = node.arguments[0];
|
|
752
|
+
if (moduleSpecifier !== undefined && typescript.isStringLiteralLike(moduleSpecifier) && isNodeBuiltinSpecifier(moduleSpecifier.text)) {
|
|
753
|
+
pushRule(node, 'determinism/no-node-builtin-import');
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (expressionText === 'eval' || expressionText === 'globalThis.eval' || expressionText === 'Function' || expressionText === 'globalThis.Function') {
|
|
758
|
+
pushRule(node, 'determinism/no-dynamic-code');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const forbiddenRule = forbiddenExpressionRule(expressionText);
|
|
762
|
+
if (forbiddenRule !== undefined) {
|
|
763
|
+
pushRule(node, forbiddenRule);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (expressionText.endsWith('.localeCompare')) {
|
|
767
|
+
pushRule(node, 'determinism/no-locale-compare');
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (expressionText === 'Object.keys' || expressionText === 'Object.values' || expressionText === 'Object.entries') {
|
|
771
|
+
if (isKeyOrderStabilized(node)) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
pushRule(node, 'determinism/no-object-key-order');
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (expressionText === 'Reflect.ownKeys' || expressionText === 'globalThis.Reflect.ownKeys') {
|
|
778
|
+
if (isKeyOrderStabilized(node)) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
pushRule(node, 'determinism/no-reflect-own-keys');
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (isNativeCollectionIterationCall(node)) {
|
|
785
|
+
pushRule(node, 'determinism/no-native-map-set-iteration');
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (isUnsafeNativeMathExpression(expressionText)) {
|
|
789
|
+
pushRule(node, 'determinism/no-trig-math');
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (expressionText.endsWith('.step') && isUncertifiedPhysicsReceiver(expressionText.slice(0, -'.step'.length))) {
|
|
793
|
+
pushRule(node, 'determinism/no-uncertified-physics-step');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (typescript.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'sort') {
|
|
797
|
+
if (node.arguments.length === 0) {
|
|
798
|
+
pushRule(node, 'determinism/no-sort-without-comparator');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (node.arguments.some(argumentContainsMathRandom)) {
|
|
802
|
+
pushRule(node, 'determinism/no-random-sort-comparator');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function indirectForbiddenCallRule(node) {
|
|
807
|
+
if (typescript.isPropertyAccessExpression(node.expression)
|
|
808
|
+
&& (node.expression.name.text === 'call' || node.expression.name.text === 'apply' || node.expression.name.text === 'bind')) {
|
|
809
|
+
const receiverText = normalizedExpressionText(node.expression.expression);
|
|
810
|
+
const rule = forbiddenAliases.get(receiverText) ?? forbiddenExpressionRule(receiverText);
|
|
811
|
+
if (rule !== undefined) {
|
|
812
|
+
return rule;
|
|
813
|
+
}
|
|
814
|
+
if (mutatingMethodAliases.has(receiverText) || isExtractedMutatingMethodExpression(receiverText)) {
|
|
815
|
+
return 'determinism/no-extracted-mutating-method';
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (expressionTextForCall(node) === 'Reflect.get') {
|
|
819
|
+
const reflected = reflectedPropertyAccessText(node);
|
|
820
|
+
if (reflected !== undefined) {
|
|
821
|
+
return forbiddenExpressionRule(reflected);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
function expressionTextForCall(node) {
|
|
827
|
+
return normalizedExpressionText(node.expression);
|
|
828
|
+
}
|
|
829
|
+
function reflectedPropertyAccessText(node) {
|
|
830
|
+
const receiver = node.arguments[0];
|
|
831
|
+
const property = node.arguments[1];
|
|
832
|
+
if (receiver === undefined || property === undefined || !typescript.isStringLiteralLike(property)) {
|
|
833
|
+
return undefined;
|
|
834
|
+
}
|
|
835
|
+
return `${normalizedExpressionText(receiver)}.${property.text}`;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Strip a global-object prefix so `window.Math.random`, `self.crypto.…`,
|
|
839
|
+
* `top.performance.now`, etc. match the same rules as bare/globalThis forms.
|
|
840
|
+
* H5 games run in a browser where window/self are the ambient globals —
|
|
841
|
+
* prefixed access must not be a rule bypass.
|
|
842
|
+
*/
|
|
843
|
+
function stripGlobalPrefix(expressionText) {
|
|
844
|
+
let stripped = expressionText;
|
|
845
|
+
let changed = true;
|
|
846
|
+
while (changed) {
|
|
847
|
+
changed = false;
|
|
848
|
+
for (const prefix of ['globalThis.', 'window.', 'self.', 'top.', 'parent.', 'frames.']) {
|
|
849
|
+
if (stripped.startsWith(prefix)) {
|
|
850
|
+
stripped = stripped.slice(prefix.length);
|
|
851
|
+
changed = true;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return stripped;
|
|
856
|
+
}
|
|
857
|
+
function forbiddenExpressionRule(rawExpressionText) {
|
|
858
|
+
const expressionText = stripGlobalPrefix(rawExpressionText);
|
|
859
|
+
if (expressionText === 'Math.random') {
|
|
860
|
+
return 'determinism/no-math-random';
|
|
861
|
+
}
|
|
862
|
+
if (expressionText === 'Date.now') {
|
|
863
|
+
return 'determinism/no-date-now';
|
|
864
|
+
}
|
|
865
|
+
if (expressionText === 'performance.now') {
|
|
866
|
+
return 'determinism/no-performance-now';
|
|
867
|
+
}
|
|
868
|
+
if (expressionText === 'setTimeout' ||
|
|
869
|
+
expressionText === 'setInterval' ||
|
|
870
|
+
expressionText === 'requestAnimationFrame') {
|
|
871
|
+
return 'determinism/no-timers';
|
|
872
|
+
}
|
|
873
|
+
if (expressionText === 'setImmediate' || expressionText === 'queueMicrotask') {
|
|
874
|
+
return 'determinism/no-async-step';
|
|
875
|
+
}
|
|
876
|
+
if (expressionText === 'Promise' ||
|
|
877
|
+
expressionText === 'Promise.resolve' ||
|
|
878
|
+
expressionText === 'Promise.reject' ||
|
|
879
|
+
expressionText === 'Promise.all' ||
|
|
880
|
+
expressionText === 'Promise.allSettled' ||
|
|
881
|
+
expressionText === 'Promise.any' ||
|
|
882
|
+
expressionText === 'Promise.race') {
|
|
883
|
+
return 'determinism/no-async-step';
|
|
884
|
+
}
|
|
885
|
+
if (expressionText === 'fetch' || expressionText === 'WebSocket') {
|
|
886
|
+
return 'determinism/no-io';
|
|
887
|
+
}
|
|
888
|
+
if (expressionText === 'require') {
|
|
889
|
+
return 'determinism/no-commonjs-require';
|
|
890
|
+
}
|
|
891
|
+
if (expressionText === 'eval' || expressionText === 'Function') {
|
|
892
|
+
return 'determinism/no-dynamic-code';
|
|
893
|
+
}
|
|
894
|
+
if (expressionText === 'process.env') {
|
|
895
|
+
return 'determinism/no-process-env';
|
|
896
|
+
}
|
|
897
|
+
if (expressionText.startsWith('Intl.')) {
|
|
898
|
+
return 'determinism/no-intl-locale';
|
|
899
|
+
}
|
|
900
|
+
if (isLocaleSensitiveFormatter(expressionText)) {
|
|
901
|
+
return 'determinism/no-locale-format';
|
|
902
|
+
}
|
|
903
|
+
if (isFloatFormatter(expressionText)) {
|
|
904
|
+
return 'determinism/no-float-format';
|
|
905
|
+
}
|
|
906
|
+
if (expressionText === 'crypto.getRandomValues' || expressionText === 'crypto.randomUUID') {
|
|
907
|
+
return 'determinism/no-crypto-random';
|
|
908
|
+
}
|
|
909
|
+
return undefined;
|
|
910
|
+
}
|
|
911
|
+
function forbiddenNamespaceRoot(rawExpressionText) {
|
|
912
|
+
const expressionText = stripGlobalPrefix(rawExpressionText);
|
|
913
|
+
if (expressionText === 'Math') {
|
|
914
|
+
return 'Math';
|
|
915
|
+
}
|
|
916
|
+
if (expressionText === 'Date') {
|
|
917
|
+
return 'Date';
|
|
918
|
+
}
|
|
919
|
+
if (expressionText === 'performance') {
|
|
920
|
+
return 'performance';
|
|
921
|
+
}
|
|
922
|
+
if (expressionText === 'crypto') {
|
|
923
|
+
return 'crypto';
|
|
924
|
+
}
|
|
925
|
+
if (expressionText === 'Intl') {
|
|
926
|
+
return 'Intl';
|
|
927
|
+
}
|
|
928
|
+
if (expressionText === 'Reflect') {
|
|
929
|
+
return 'Reflect';
|
|
930
|
+
}
|
|
931
|
+
if (expressionText === 'globalThis' || rawExpressionText === 'globalThis') {
|
|
932
|
+
return 'globalThis';
|
|
933
|
+
}
|
|
934
|
+
if (rawExpressionText !== expressionText && expressionText === '') {
|
|
935
|
+
// A bare window/self/top/parent/frames reference is the same escape
|
|
936
|
+
// hatch as bare globalThis.
|
|
937
|
+
return 'globalThis';
|
|
938
|
+
}
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
function isNodeBuiltinSpecifier(specifier) {
|
|
942
|
+
const normalized = specifier.startsWith('node:') ? specifier.slice('node:'.length) : specifier;
|
|
943
|
+
const slashIndex = normalized.indexOf('/');
|
|
944
|
+
const rootSpecifier = slashIndex === -1 ? normalized : normalized.slice(0, slashIndex);
|
|
945
|
+
return nodeBuiltinSpecifiers.has(normalized) || nodeBuiltinSpecifiers.has(rootSpecifier);
|
|
946
|
+
}
|
|
947
|
+
function isFloatFormatter(expressionText) {
|
|
948
|
+
// toFixed/toPrecision half-way rounding has diverged across JS engines
|
|
949
|
+
// (Hermes-native vs V8-web both ship here) — the most likely accidental
|
|
950
|
+
// desync a creator introduces when quantizing state. The deterministic
|
|
951
|
+
// fixed-point helper is itself named toFixed (ctx.math.toFixed) — a
|
|
952
|
+
// `math`-suffixed receiver is that API, not Number.prototype.toFixed.
|
|
953
|
+
if (expressionText.endsWith('math.toFixed')) {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
return expressionText.endsWith('.toFixed') || expressionText.endsWith('.toPrecision');
|
|
957
|
+
}
|
|
958
|
+
function isLocaleSensitiveFormatter(expressionText) {
|
|
959
|
+
return expressionText.endsWith('.toLocaleString')
|
|
960
|
+
|| expressionText.endsWith('.toLocaleDateString')
|
|
961
|
+
|| expressionText.endsWith('.toLocaleTimeString')
|
|
962
|
+
|| expressionText.endsWith('.toLocaleLowerCase')
|
|
963
|
+
|| expressionText.endsWith('.toLocaleUpperCase');
|
|
964
|
+
}
|
|
965
|
+
function isNativeCollectionIterationCall(node) {
|
|
966
|
+
if (!isDeterministicStepScope()) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
if (typescript.isPropertyAccessExpression(node.expression)) {
|
|
970
|
+
const methodName = node.expression.name.text;
|
|
971
|
+
if (methodName === 'keys' || methodName === 'values' || methodName === 'entries' || methodName === 'forEach') {
|
|
972
|
+
const collectionKind = nativeCollectionExpressionKind(node.expression.expression);
|
|
973
|
+
return collectionKind === 'Map' || collectionKind === 'Set';
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const expressionText = normalizedExpressionText(node.expression);
|
|
977
|
+
if (expressionText === 'Array.from' || expressionText === 'globalThis.Array.from') {
|
|
978
|
+
const firstArgument = node.arguments[0];
|
|
979
|
+
if (firstArgument === undefined) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
const collectionKind = nativeCollectionExpressionKind(firstArgument);
|
|
983
|
+
return collectionKind === 'Map' || collectionKind === 'Set';
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
function isUnsafeNativeMathExpression(rawExpressionText) {
|
|
988
|
+
return unsafeNativeMathExpressions.has(stripGlobalPrefix(rawExpressionText));
|
|
989
|
+
}
|
|
990
|
+
function isAllowedCertificationTimerCall(expressionText) {
|
|
991
|
+
return stripGlobalPrefix(expressionText) === 'performance.now'
|
|
992
|
+
&& functionStack.at(-1) === 'engineCompleteNowMs';
|
|
993
|
+
}
|
|
994
|
+
function functionName(node) {
|
|
995
|
+
if (typescript.isFunctionDeclaration(node) || typescript.isFunctionExpression(node)) {
|
|
996
|
+
return node.name?.text ?? '';
|
|
997
|
+
}
|
|
998
|
+
if (typescript.isMethodDeclaration(node)) {
|
|
999
|
+
return propertyNameText(node.name);
|
|
1000
|
+
}
|
|
1001
|
+
if (typescript.isVariableDeclaration(node.parent) && typescript.isIdentifier(node.parent.name)) {
|
|
1002
|
+
return node.parent.name.text;
|
|
1003
|
+
}
|
|
1004
|
+
return '';
|
|
1005
|
+
}
|
|
1006
|
+
function propertyNameText(name) {
|
|
1007
|
+
if (typescript.isIdentifier(name) ||
|
|
1008
|
+
typescript.isStringLiteral(name) ||
|
|
1009
|
+
typescript.isNumericLiteral(name) ||
|
|
1010
|
+
typescript.isPrivateIdentifier(name)) {
|
|
1011
|
+
return name.text;
|
|
1012
|
+
}
|
|
1013
|
+
return '';
|
|
1014
|
+
}
|
|
1015
|
+
function argumentContainsMathRandom(node) {
|
|
1016
|
+
let found = false;
|
|
1017
|
+
const scan = (candidate) => {
|
|
1018
|
+
if (typescript.isCallExpression(candidate) &&
|
|
1019
|
+
normalizedExpressionText(candidate.expression) === 'Math.random') {
|
|
1020
|
+
found = true;
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
typescript.forEachChild(candidate, scan);
|
|
1024
|
+
};
|
|
1025
|
+
scan(node);
|
|
1026
|
+
return found;
|
|
1027
|
+
}
|
|
1028
|
+
function isKeyOrderStabilized(node) {
|
|
1029
|
+
let current = node;
|
|
1030
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
1031
|
+
const propertyAccess = current.parent;
|
|
1032
|
+
if (!typescript.isPropertyAccessExpression(propertyAccess)) {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
const call = propertyAccess.parent;
|
|
1036
|
+
if (!typescript.isCallExpression(call)) {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
if (propertyAccess.name.text === 'sort') {
|
|
1040
|
+
return call.arguments.length > 0;
|
|
1041
|
+
}
|
|
1042
|
+
if (propertyAccess.name.text !== 'filter' && propertyAccess.name.text !== 'map') {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
current = call;
|
|
1046
|
+
}
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
function checkStateFieldAccess(node, stateFields) {
|
|
1050
|
+
const ruleId = 'determinism/no-undeclared-state-field';
|
|
1051
|
+
if (typescript.isPropertyAccessExpression(node) &&
|
|
1052
|
+
typescript.isIdentifier(node.expression) &&
|
|
1053
|
+
node.expression.text === schemaOptions.stateObjectName &&
|
|
1054
|
+
!stateFields.has(node.name.text)) {
|
|
1055
|
+
pushDiagnostic(node, ruleId, `State field ${node.name.text} is not declared in the deterministic state schema.`, 'Declare the field in descriptor.state.fields or keep it outside checksummed simulation state.');
|
|
1056
|
+
}
|
|
1057
|
+
if (typescript.isElementAccessExpression(node) &&
|
|
1058
|
+
typescript.isIdentifier(node.expression) &&
|
|
1059
|
+
node.expression.text === schemaOptions.stateObjectName &&
|
|
1060
|
+
node.argumentExpression !== undefined &&
|
|
1061
|
+
typescript.isStringLiteralLike(node.argumentExpression) &&
|
|
1062
|
+
!stateFields.has(node.argumentExpression.text)) {
|
|
1063
|
+
pushDiagnostic(node, ruleId, `State field ${node.argumentExpression.text} is not declared in the deterministic state schema.`, 'Declare the field in descriptor.state.fields or keep it outside checksummed simulation state.');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function normalizedExpressionText(node) {
|
|
1067
|
+
if (typescript.isIdentifier(node)) {
|
|
1068
|
+
return namespaceAliases.get(node.text) ?? node.text;
|
|
1069
|
+
}
|
|
1070
|
+
if (typescript.isPropertyAccessExpression(node)) {
|
|
1071
|
+
return `${normalizedExpressionText(node.expression)}.${node.name.text}`;
|
|
1072
|
+
}
|
|
1073
|
+
if (typescript.isElementAccessExpression(node)) {
|
|
1074
|
+
const key = literalPropertyName(node.argumentExpression);
|
|
1075
|
+
return key === undefined ? node.getText(sourceFile) : `${normalizedExpressionText(node.expression)}.${key}`;
|
|
1076
|
+
}
|
|
1077
|
+
return node.getText(sourceFile);
|
|
1078
|
+
}
|
|
1079
|
+
function literalPropertyName(node) {
|
|
1080
|
+
/* istanbul ignore next -- ElementAccessExpression argumentExpression is present for parseable element access. */
|
|
1081
|
+
if (node === undefined) {
|
|
1082
|
+
return undefined;
|
|
1083
|
+
}
|
|
1084
|
+
if (typescript.isStringLiteralLike(node)) {
|
|
1085
|
+
return node.text;
|
|
1086
|
+
}
|
|
1087
|
+
return undefined;
|
|
1088
|
+
}
|
|
1089
|
+
function pushRule(node, ruleId) {
|
|
1090
|
+
const rule = rules.find((candidate) => candidate.ruleId === ruleId);
|
|
1091
|
+
/* istanbul ignore next -- internal callers pass rule ids from the static rule table. */
|
|
1092
|
+
if (rule === undefined) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
pushDiagnostic(node, rule.ruleId, rule.message, rule.suggestion, rule.severity);
|
|
1096
|
+
}
|
|
1097
|
+
function pushDiagnostic(node, ruleId, message, suggestion, defaultSeverity = 'error') {
|
|
1098
|
+
const start = node.getStart(sourceFile);
|
|
1099
|
+
const location = sourceFile.getLineAndCharacterOfPosition(start);
|
|
1100
|
+
const line = location.line + 1;
|
|
1101
|
+
const suppression = parseSuppression(getLine(content, line - 1), ruleId);
|
|
1102
|
+
if (suppression === 'valid') {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (suppression === 'invalid') {
|
|
1106
|
+
diagnostics.push({
|
|
1107
|
+
ruleId: 'determinism/invalid-suppression',
|
|
1108
|
+
severity: 'error',
|
|
1109
|
+
path,
|
|
1110
|
+
line,
|
|
1111
|
+
column: 1,
|
|
1112
|
+
message: 'Determinism suppression comments must name a rule and include a reason.',
|
|
1113
|
+
suggestion: 'Use // rdm-ignore-next-line <rule-id>: <reason> directly above the suppressed line.',
|
|
1114
|
+
});
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
diagnostics.push({
|
|
1118
|
+
ruleId,
|
|
1119
|
+
severity: mode === 'strict' || defaultSeverity === 'error' ? 'error' : 'warn',
|
|
1120
|
+
path,
|
|
1121
|
+
line,
|
|
1122
|
+
column: location.character + 1,
|
|
1123
|
+
message,
|
|
1124
|
+
suggestion,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function parseDiagnosticsToDeterminismDiagnostics(typescript, sourceFile, path) {
|
|
1129
|
+
/* istanbul ignore next -- TypeScript SourceFile always exposes parseDiagnostics; fallback protects unusual host shims. */
|
|
1130
|
+
const parseDiagnostics = sourceFile.parseDiagnostics ?? [];
|
|
1131
|
+
return parseDiagnostics.map((diagnostic) => {
|
|
1132
|
+
/* istanbul ignore next -- TypeScript parse diagnostics include start offsets for source-file syntax errors. */
|
|
1133
|
+
const start = diagnostic.start ?? 0;
|
|
1134
|
+
const position = sourceFile.getLineAndCharacterOfPosition(start);
|
|
1135
|
+
const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, ' ');
|
|
1136
|
+
return {
|
|
1137
|
+
ruleId: 'determinism/syntax-error',
|
|
1138
|
+
severity: 'error',
|
|
1139
|
+
path,
|
|
1140
|
+
line: position.line + 1,
|
|
1141
|
+
column: position.character + 1,
|
|
1142
|
+
message: `TypeScript syntax error: ${message}`,
|
|
1143
|
+
suggestion: 'Fix the syntax error before certifying deterministic simulation code.',
|
|
1144
|
+
};
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
function checkFileWithRegex(path, content, mode, schemaOptions) {
|
|
1148
|
+
const diagnostics = [];
|
|
1149
|
+
const lines = content.split(/\r?\n/);
|
|
1150
|
+
lines.forEach((lineText, lineIndex) => {
|
|
1151
|
+
const sourceLine = lineIndex + 1;
|
|
1152
|
+
const checkableLineText = maskNonCodeText(lineText);
|
|
1153
|
+
for (const rule of rules) {
|
|
1154
|
+
rule.pattern.lastIndex = 0;
|
|
1155
|
+
let match = rule.pattern.exec(checkableLineText);
|
|
1156
|
+
while (match !== null) {
|
|
1157
|
+
const suppression = parseSuppression(lines[lineIndex - 1], rule.ruleId);
|
|
1158
|
+
if (suppression === 'valid') {
|
|
1159
|
+
match = rule.pattern.exec(checkableLineText);
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
if (suppression === 'invalid') {
|
|
1163
|
+
diagnostics.push({
|
|
1164
|
+
ruleId: 'determinism/invalid-suppression',
|
|
1165
|
+
severity: 'error',
|
|
1166
|
+
path,
|
|
1167
|
+
line: sourceLine,
|
|
1168
|
+
column: 1,
|
|
1169
|
+
message: 'Determinism suppression comments must name a rule and include a reason.',
|
|
1170
|
+
suggestion: 'Use // rdm-ignore-next-line <rule-id>: <reason> directly above the suppressed line.',
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
diagnostics.push({
|
|
1175
|
+
ruleId: rule.ruleId,
|
|
1176
|
+
severity: mode === 'strict' || rule.severity === 'error' ? 'error' : 'warn',
|
|
1177
|
+
path,
|
|
1178
|
+
line: sourceLine,
|
|
1179
|
+
column: match.index + 1,
|
|
1180
|
+
message: rule.message,
|
|
1181
|
+
suggestion: rule.suggestion,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
match = rule.pattern.exec(checkableLineText);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
diagnostics.push(...checkDeclaredStateFields(path, checkableLineText, lines[lineIndex - 1], sourceLine, mode, schemaOptions));
|
|
1188
|
+
});
|
|
1189
|
+
return diagnostics;
|
|
1190
|
+
}
|
|
1191
|
+
function getLine(content, line) {
|
|
1192
|
+
return content.split(/\r?\n/)[line - 1];
|
|
1193
|
+
}
|
|
1194
|
+
function isUncertifiedPhysicsReceiver(receiver) {
|
|
1195
|
+
return receiver === 'world' || receiver === 'physicsWorld' || receiver === 'rapierWorld' || receiver === 'matterWorld';
|
|
1196
|
+
}
|
|
1197
|
+
function isRenderObjectReceiver(receiver) {
|
|
1198
|
+
return receiver === 'scene' || receiver === 'mesh' || receiver === 'camera' || receiver === 'group' || receiver === 'object3D';
|
|
1199
|
+
}
|
|
1200
|
+
function createSchemaOptions(schemaStateFields, stateObjectName = 'state') {
|
|
1201
|
+
return {
|
|
1202
|
+
stateFields: schemaStateFields === undefined ? undefined : new Set(schemaStateFields),
|
|
1203
|
+
stateObjectName,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function checkDeclaredStateFields(path, lineText, previousLine, sourceLine, mode, options) {
|
|
1207
|
+
if (options.stateFields === undefined) {
|
|
1208
|
+
return [];
|
|
1209
|
+
}
|
|
1210
|
+
const diagnostics = [];
|
|
1211
|
+
const ruleId = 'determinism/no-undeclared-state-field';
|
|
1212
|
+
const stateName = escapeRegExp(options.stateObjectName);
|
|
1213
|
+
const dotPattern = new RegExp(`\\b${stateName}\\.([A-Za-z_$][A-Za-z0-9_$]*)`, 'g');
|
|
1214
|
+
const bracketPattern = new RegExp(`\\b${stateName}\\s*\\[\\s*['"]([^'"]+)['"]\\s*\\]`, 'g');
|
|
1215
|
+
const hasStateAccess = dotPattern.test(lineText) || bracketPattern.test(lineText);
|
|
1216
|
+
dotPattern.lastIndex = 0;
|
|
1217
|
+
bracketPattern.lastIndex = 0;
|
|
1218
|
+
if (!hasStateAccess) {
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
const suppression = parseSuppression(previousLine, ruleId);
|
|
1222
|
+
if (suppression === 'valid') {
|
|
1223
|
+
return [];
|
|
1224
|
+
}
|
|
1225
|
+
if (suppression === 'invalid') {
|
|
1226
|
+
diagnostics.push({
|
|
1227
|
+
ruleId: 'determinism/invalid-suppression',
|
|
1228
|
+
severity: 'error',
|
|
1229
|
+
path,
|
|
1230
|
+
line: sourceLine,
|
|
1231
|
+
column: 1,
|
|
1232
|
+
message: 'Determinism suppression comments must name a rule and include a reason.',
|
|
1233
|
+
suggestion: 'Use // rdm-ignore-next-line <rule-id>: <reason> directly above the suppressed line.',
|
|
1234
|
+
});
|
|
1235
|
+
return diagnostics;
|
|
1236
|
+
}
|
|
1237
|
+
collectStateFieldDiagnostics(dotPattern, path, sourceLine, mode, options.stateFields, diagnostics);
|
|
1238
|
+
collectStateFieldDiagnostics(bracketPattern, path, sourceLine, mode, options.stateFields, diagnostics);
|
|
1239
|
+
return diagnostics;
|
|
1240
|
+
function collectStateFieldDiagnostics(pattern, diagnosticPath, diagnosticLine, diagnosticMode, stateFields, target) {
|
|
1241
|
+
let match = pattern.exec(lineText);
|
|
1242
|
+
while (match !== null) {
|
|
1243
|
+
const field = match[1];
|
|
1244
|
+
if (!stateFields.has(field)) {
|
|
1245
|
+
target.push({
|
|
1246
|
+
ruleId,
|
|
1247
|
+
severity: diagnosticMode === 'strict' ? 'error' : 'warn',
|
|
1248
|
+
path: diagnosticPath,
|
|
1249
|
+
line: diagnosticLine,
|
|
1250
|
+
column: match.index + 1,
|
|
1251
|
+
message: `State field ${field} is not declared in the deterministic state schema.`,
|
|
1252
|
+
suggestion: 'Declare the field in descriptor.state.fields or keep it outside checksummed simulation state.',
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
match = pattern.exec(lineText);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async function collectFiles(paths, followImports, project) {
|
|
1260
|
+
const files = [];
|
|
1261
|
+
const visited = new Set();
|
|
1262
|
+
const resolver = await createImportResolver(project);
|
|
1263
|
+
for (const path of paths) {
|
|
1264
|
+
const entry = await (0, promises_1.stat)(path);
|
|
1265
|
+
if (entry.isFile() && isCheckableFile(path)) {
|
|
1266
|
+
if (followImports) {
|
|
1267
|
+
files.push(...(await collectFileWithImports(path, visited, resolver)));
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
files.push(path);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (entry.isDirectory()) {
|
|
1274
|
+
files.push(...(await collectDirectory(path)));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return Array.from(new Set(files)).sort();
|
|
1278
|
+
}
|
|
1279
|
+
async function collectDirectory(path) {
|
|
1280
|
+
const files = [];
|
|
1281
|
+
const entries = await (0, promises_1.readdir)(path);
|
|
1282
|
+
for (const entry of entries) {
|
|
1283
|
+
if (entry === 'node_modules' || entry === 'dist' || entry === 'artifacts') {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const childPath = `${path}/${entry}`;
|
|
1287
|
+
const child = await (0, promises_1.stat)(childPath);
|
|
1288
|
+
if (child.isDirectory()) {
|
|
1289
|
+
files.push(...(await collectDirectory(childPath)));
|
|
1290
|
+
}
|
|
1291
|
+
else if (child.isFile() && isCheckableFile(childPath)) {
|
|
1292
|
+
files.push(childPath);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return files;
|
|
1296
|
+
}
|
|
1297
|
+
function isCheckableFile(path) {
|
|
1298
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(path);
|
|
1299
|
+
}
|
|
1300
|
+
async function collectFileWithImports(path, visited, resolver) {
|
|
1301
|
+
const absolutePath = (0, node_path_1.resolve)(path);
|
|
1302
|
+
if (visited.has(absolutePath)) {
|
|
1303
|
+
return [];
|
|
1304
|
+
}
|
|
1305
|
+
visited.add(absolutePath);
|
|
1306
|
+
const files = [path];
|
|
1307
|
+
const content = await (0, promises_1.readFile)(path, 'utf8');
|
|
1308
|
+
for (const specifier of findStaticImportSpecifiers(content)) {
|
|
1309
|
+
const resolvedPath = await resolver.resolve(path, specifier);
|
|
1310
|
+
if (resolvedPath !== undefined) {
|
|
1311
|
+
files.push(...(await collectFileWithImports(resolvedPath, visited, resolver)));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return files;
|
|
1315
|
+
}
|
|
1316
|
+
function findStaticImportSpecifiers(content) {
|
|
1317
|
+
const specifiers = [];
|
|
1318
|
+
const importPattern = /^\s*(?:import|export)\s+(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
1319
|
+
let match = importPattern.exec(content);
|
|
1320
|
+
while (match !== null) {
|
|
1321
|
+
specifiers.push(match[1]);
|
|
1322
|
+
match = importPattern.exec(content);
|
|
1323
|
+
}
|
|
1324
|
+
return specifiers;
|
|
1325
|
+
}
|
|
1326
|
+
async function createImportResolver(project) {
|
|
1327
|
+
const relativeResolver = {
|
|
1328
|
+
resolve(containingFile, specifier) {
|
|
1329
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
1330
|
+
return Promise.resolve(undefined);
|
|
1331
|
+
}
|
|
1332
|
+
return resolveImportPath((0, node_path_1.dirname)(containingFile), specifier);
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
if (project === undefined) {
|
|
1336
|
+
return relativeResolver;
|
|
1337
|
+
}
|
|
1338
|
+
const typescript = requireModule('typescript');
|
|
1339
|
+
const projectPath = (0, node_path_1.resolve)(project);
|
|
1340
|
+
const config = typescript.readConfigFile(projectPath, typescript.sys.readFile);
|
|
1341
|
+
if (config.error !== undefined) {
|
|
1342
|
+
const message = typescript.flattenDiagnosticMessageText(config.error.messageText, '\n');
|
|
1343
|
+
throw new Error(`Unable to read deterministic project ${projectPath}: ${message}`);
|
|
1344
|
+
}
|
|
1345
|
+
const parsed = typescript.parseJsonConfigFileContent(config.config, typescript.sys, (0, node_path_1.dirname)(projectPath));
|
|
1346
|
+
const compilerOptions = parsed.options;
|
|
1347
|
+
const host = typescript.createCompilerHost(compilerOptions, true);
|
|
1348
|
+
return {
|
|
1349
|
+
async resolve(containingFile, specifier) {
|
|
1350
|
+
const resolved = typescript.resolveModuleName(specifier, (0, node_path_1.resolve)(containingFile), compilerOptions, host).resolvedModule;
|
|
1351
|
+
if (resolved !== undefined && !resolved.isExternalLibraryImport && isCheckableFile(resolved.resolvedFileName)) {
|
|
1352
|
+
return resolved.resolvedFileName;
|
|
1353
|
+
}
|
|
1354
|
+
return relativeResolver.resolve(containingFile, specifier);
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
async function resolveImportPath(baseDirectory, specifier) {
|
|
1359
|
+
const candidate = (0, node_path_1.resolve)(baseDirectory, specifier);
|
|
1360
|
+
const sourceCandidate = candidate.replace(/\.(js|jsx|mjs|cjs)$/, '');
|
|
1361
|
+
const paths = [
|
|
1362
|
+
candidate,
|
|
1363
|
+
`${sourceCandidate}.ts`,
|
|
1364
|
+
`${sourceCandidate}.tsx`,
|
|
1365
|
+
`${candidate}.ts`,
|
|
1366
|
+
`${candidate}.tsx`,
|
|
1367
|
+
`${candidate}.js`,
|
|
1368
|
+
`${candidate}.jsx`,
|
|
1369
|
+
`${candidate}.mjs`,
|
|
1370
|
+
`${candidate}.cjs`,
|
|
1371
|
+
`${candidate}/index.ts`,
|
|
1372
|
+
`${candidate}/index.tsx`,
|
|
1373
|
+
`${candidate}/index.js`,
|
|
1374
|
+
`${candidate}/index.jsx`,
|
|
1375
|
+
];
|
|
1376
|
+
for (const path of paths) {
|
|
1377
|
+
try {
|
|
1378
|
+
const entry = await (0, promises_1.stat)(path);
|
|
1379
|
+
if (entry.isFile() && isCheckableFile(path)) {
|
|
1380
|
+
return path;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
catch {
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return undefined;
|
|
1388
|
+
}
|
|
1389
|
+
function parseSuppression(previousLine, ruleId) {
|
|
1390
|
+
if (previousLine === undefined || !previousLine.includes('rdm-ignore-next-line')) {
|
|
1391
|
+
return 'none';
|
|
1392
|
+
}
|
|
1393
|
+
const suppression = parseSuppressionComment(previousLine);
|
|
1394
|
+
if (suppression === undefined || suppression.ruleId !== ruleId || suppression.reason.length < 8) {
|
|
1395
|
+
return 'invalid';
|
|
1396
|
+
}
|
|
1397
|
+
return 'valid';
|
|
1398
|
+
}
|
|
1399
|
+
function parseSuppressionDetails(line) {
|
|
1400
|
+
if (!line.includes('rdm-ignore-next-line')) {
|
|
1401
|
+
return undefined;
|
|
1402
|
+
}
|
|
1403
|
+
const suppression = parseSuppressionComment(line);
|
|
1404
|
+
if (suppression === undefined || suppression.reason.length < 8) {
|
|
1405
|
+
return undefined;
|
|
1406
|
+
}
|
|
1407
|
+
return suppression;
|
|
1408
|
+
}
|
|
1409
|
+
function parseSuppressionComment(line) {
|
|
1410
|
+
const markerIndex = line.indexOf('rdm-ignore-next-line');
|
|
1411
|
+
let cursor = markerIndex + 'rdm-ignore-next-line'.length;
|
|
1412
|
+
while (cursor < line.length && isInlineWhitespace(line[cursor])) {
|
|
1413
|
+
cursor += 1;
|
|
1414
|
+
}
|
|
1415
|
+
const ruleStart = cursor;
|
|
1416
|
+
while (cursor < line.length && !isInlineWhitespace(line[cursor]) && line[cursor] !== ':') {
|
|
1417
|
+
cursor += 1;
|
|
1418
|
+
}
|
|
1419
|
+
const ruleId = line.slice(ruleStart, cursor);
|
|
1420
|
+
while (cursor < line.length && isInlineWhitespace(line[cursor])) {
|
|
1421
|
+
cursor += 1;
|
|
1422
|
+
}
|
|
1423
|
+
if (ruleId.length === 0 || line[cursor] !== ':') {
|
|
1424
|
+
return undefined;
|
|
1425
|
+
}
|
|
1426
|
+
const reason = line.slice(cursor + 1).trim();
|
|
1427
|
+
return { ruleId, reason };
|
|
1428
|
+
}
|
|
1429
|
+
function isInlineWhitespace(value) {
|
|
1430
|
+
return value === ' ' || value === '\t';
|
|
1431
|
+
}
|
|
1432
|
+
function escapeRegExp(value) {
|
|
1433
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1434
|
+
}
|
|
1435
|
+
function maskNonCodeText(lineText) {
|
|
1436
|
+
let result = '';
|
|
1437
|
+
let quote;
|
|
1438
|
+
let escaped = false;
|
|
1439
|
+
for (let index = 0; index < lineText.length; index += 1) {
|
|
1440
|
+
const char = lineText[index];
|
|
1441
|
+
const next = lineText[index + 1];
|
|
1442
|
+
if (quote === undefined && char === '/' && next === '/') {
|
|
1443
|
+
return `${result}${' '.repeat(lineText.length - result.length)}`;
|
|
1444
|
+
}
|
|
1445
|
+
if (quote === undefined && (char === '"' || char === "'" || char === '`')) {
|
|
1446
|
+
quote = char;
|
|
1447
|
+
result += ' ';
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (quote !== undefined) {
|
|
1451
|
+
result += ' ';
|
|
1452
|
+
if (escaped) {
|
|
1453
|
+
escaped = false;
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (char === '\\') {
|
|
1457
|
+
escaped = true;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (char === quote) {
|
|
1461
|
+
quote = undefined;
|
|
1462
|
+
}
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
result += char;
|
|
1466
|
+
}
|
|
1467
|
+
return result;
|
|
1468
|
+
}
|