@ox0/guards 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1023 @@
1
+ import * as __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__ from "node:fs";
2
+ import * as __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__ from "node:path";
3
+ import * as __WEBPACK_EXTERNAL_MODULE_oxc_parser_1c05de9b__ from "oxc-parser";
4
+ function isNode(value) {
5
+ return Boolean(value) && "object" == typeof value && "string" == typeof value.type;
6
+ }
7
+ function walkChild(child, visitor, parent) {
8
+ if (Array.isArray(child)) {
9
+ for (const entry of child)walk(entry, visitor, parent);
10
+ return;
11
+ }
12
+ walk(child, visitor, parent);
13
+ }
14
+ function walk(node, visitor, parent = null) {
15
+ if (!isNode(node)) return;
16
+ visitor(node, parent);
17
+ for (const [key, value] of Object.entries(node))if ("parent" !== key) walkChild(value, visitor, node);
18
+ }
19
+ function getIdentifierName(node) {
20
+ if (!isNode(node)) return null;
21
+ if ("Identifier" === node.type || "JSXIdentifier" === node.type) return "string" == typeof node.name ? node.name : null;
22
+ return null;
23
+ }
24
+ function getCalleeName(node) {
25
+ if (!isNode(node)) return null;
26
+ if ("Identifier" === node.type) return "string" == typeof node.name ? node.name : null;
27
+ if ("MemberExpression" === node.type) {
28
+ const objectName = getCalleeName(node.object);
29
+ const propertyName = getIdentifierName(node.property);
30
+ return objectName && propertyName ? `${objectName}.${propertyName}` : null;
31
+ }
32
+ return null;
33
+ }
34
+ function getJsxElementName(node) {
35
+ if (!isNode(node)) return null;
36
+ if ("JSXIdentifier" === node.type) return "string" == typeof node.name ? node.name : null;
37
+ if ("JSXMemberExpression" === node.type) {
38
+ const objectName = getJsxElementName(node.object);
39
+ const propertyName = getJsxElementName(node.property);
40
+ return objectName && propertyName ? `${objectName}.${propertyName}` : null;
41
+ }
42
+ return null;
43
+ }
44
+ function getStringLiteralValue(node) {
45
+ if (!isNode(node)) return null;
46
+ if ("Literal" === node.type && "string" == typeof node.value) return node.value;
47
+ if ("TemplateLiteral" === node.type && Array.isArray(node.expressions) && 0 === node.expressions.length) {
48
+ const quasis = Array.isArray(node.quasis) ? node.quasis : [];
49
+ return quasis.map((quasi)=>{
50
+ if (!isNode(quasi)) return "";
51
+ const value = quasi.value;
52
+ if (!value || "object" != typeof value || "string" != typeof value.cooked) return "";
53
+ return value.cooked;
54
+ }).join("");
55
+ }
56
+ return null;
57
+ }
58
+ function isFunctionLike(node) {
59
+ return isNode(node) && ("FunctionDeclaration" === node.type || "FunctionExpression" === node.type || "ArrowFunctionExpression" === node.type);
60
+ }
61
+ function getFunctionName(node, parent) {
62
+ if (!isNode(node)) return null;
63
+ if ("id" in node && isNode(node.id) && "string" == typeof node.id.name) return node.id.name;
64
+ if (isNode(parent) && "VariableDeclarator" === parent.type && isNode(parent.id) && "Identifier" === parent.id.type && "string" == typeof parent.id.name) return parent.id.name;
65
+ return null;
66
+ }
67
+ const GUARD_HELPERS = {
68
+ walk,
69
+ getCalleeName,
70
+ getJsxElementName,
71
+ getStringLiteralValue,
72
+ isFunctionLike,
73
+ getFunctionName
74
+ };
75
+ const LABELABLE_ELEMENTS = new Set([
76
+ "input",
77
+ "select",
78
+ "textarea"
79
+ ]);
80
+ const LABEL_ATTRIBUTES = new Set([
81
+ "aria-label",
82
+ "aria-labelledby"
83
+ ]);
84
+ const ALLOWED_COMPONENT_PATHS = new Set([
85
+ "packages/ui/src/components/text-field.tsx"
86
+ ]);
87
+ function hasUsefulAttribute(node, helpers) {
88
+ const attributes = Array.isArray(node.attributes) ? node.attributes : [];
89
+ for (const attribute of attributes){
90
+ if (!attribute || "object" != typeof attribute) continue;
91
+ const name = helpers.getJsxElementName(attribute.name);
92
+ if (name && LABEL_ATTRIBUTES.has(name)) return true;
93
+ }
94
+ return false;
95
+ }
96
+ const createMissingLabelGuard = ()=>[
97
+ "guard/missing-label",
98
+ {
99
+ fileExtensions: [
100
+ ".jsx",
101
+ ".tsx"
102
+ ],
103
+ tokenPrefilter (source) {
104
+ return source.includes("<input") || source.includes("<select") || source.includes("<textarea");
105
+ },
106
+ evaluate ({ helpers, program, relativeFilePath }) {
107
+ if (ALLOWED_COMPONENT_PATHS.has(relativeFilePath.split("\\").join("/"))) return [];
108
+ const findings = [];
109
+ helpers.walk(program, (node, parent)=>{
110
+ if ("JSXElement" !== node.type) return;
111
+ const openingElement = node.openingElement;
112
+ if (!openingElement) return;
113
+ const elementName = helpers.getJsxElementName(openingElement.name);
114
+ if (!elementName || !LABELABLE_ELEMENTS.has(elementName)) return;
115
+ if (hasUsefulAttribute(openingElement, helpers)) return;
116
+ const wrappedByLabel = parent?.type === "JSXElement" && "label" === helpers.getJsxElementName(parent.openingElement.name);
117
+ if (wrappedByLabel) return;
118
+ findings.push({
119
+ start: openingElement.start,
120
+ end: openingElement.end,
121
+ message: "Form controls must have a visible label or an accessible label via aria-label or aria-labelledby."
122
+ });
123
+ });
124
+ return findings;
125
+ }
126
+ }
127
+ ];
128
+ function isPascalCase(name) {
129
+ return Boolean(name) && /^[A-Z]/.test(name ?? "");
130
+ }
131
+ const createNoAsyncReactComponentGuard = ()=>[
132
+ "guard/no-async-react-component",
133
+ {
134
+ fileExtensions: [
135
+ ".jsx",
136
+ ".tsx"
137
+ ],
138
+ tokenPrefilter (source) {
139
+ return source.includes("async");
140
+ },
141
+ evaluate ({ helpers, program }) {
142
+ const findings = [];
143
+ helpers.walk(program, (node, parent)=>{
144
+ if (!helpers.isFunctionLike(node)) return;
145
+ if (true !== node.async) return;
146
+ const name = helpers.getFunctionName(node, parent);
147
+ if (!isPascalCase(name)) return;
148
+ findings.push({
149
+ start: node.start,
150
+ end: node.end,
151
+ message: "React components should not be async. Move async work into loaders, actions, or effects."
152
+ });
153
+ });
154
+ return findings;
155
+ }
156
+ }
157
+ ];
158
+ const INTL_CONSTRUCTORS = new Set([
159
+ "Intl.DateTimeFormat",
160
+ "Intl.NumberFormat",
161
+ "Intl.RelativeTimeFormat",
162
+ "Intl.PluralRules",
163
+ "Intl.Collator",
164
+ "Intl.ListFormat",
165
+ "Intl.Segmenter"
166
+ ]);
167
+ const ALLOWED_PATHS = [
168
+ "packages/i18n/"
169
+ ];
170
+ function isAllowedPath(relativeFilePath) {
171
+ const normalized = relativeFilePath.split("\\").join("/");
172
+ return ALLOWED_PATHS.some((prefix)=>normalized.startsWith(prefix));
173
+ }
174
+ const createNoBareIntlGuard = ()=>[
175
+ "guard/no-bare-intl",
176
+ {
177
+ fileExtensions: [
178
+ ".js",
179
+ ".jsx",
180
+ ".ts",
181
+ ".tsx",
182
+ ".mjs",
183
+ ".cjs",
184
+ ".mts",
185
+ ".cts"
186
+ ],
187
+ tokenPrefilter (source) {
188
+ return source.includes("new Intl.");
189
+ },
190
+ evaluate ({ helpers, program, relativeFilePath }) {
191
+ if (isAllowedPath(relativeFilePath)) return [];
192
+ const findings = [];
193
+ helpers.walk(program, (node)=>{
194
+ if ("NewExpression" !== node.type) return;
195
+ const calleeName = helpers.getCalleeName(node.callee);
196
+ if (!calleeName || !INTL_CONSTRUCTORS.has(calleeName)) return;
197
+ findings.push({
198
+ start: node.start,
199
+ end: node.end,
200
+ message: `Do not construct bare ${calleeName} instances. Intl constructors are slow; use the caching helpers from the i18n package instead.`
201
+ });
202
+ });
203
+ return findings;
204
+ }
205
+ }
206
+ ];
207
+ const CSS_PATTERN = /\.(css|scss|less)$/i;
208
+ const ALLOWED_SPECIFIERS = new Set([
209
+ "@ox0/tokens/styles.css"
210
+ ]);
211
+ const ALLOWED_FILE_PATHS = new Set([
212
+ "packages/ui/src/providers/ox0-provider.tsx"
213
+ ]);
214
+ function isAllowedImport(relativeFilePath, sourceValue) {
215
+ const normalizedPath = relativeFilePath.split("\\").join("/");
216
+ return Boolean(sourceValue) && ALLOWED_SPECIFIERS.has(sourceValue ?? "") && ALLOWED_FILE_PATHS.has(normalizedPath);
217
+ }
218
+ function isCssSpecifier(value) {
219
+ return Boolean(value && CSS_PATTERN.test(value));
220
+ }
221
+ const createNoCssImportGuard = ()=>[
222
+ "guard/no-css-import",
223
+ {
224
+ fileExtensions: [
225
+ ".js",
226
+ ".jsx",
227
+ ".ts",
228
+ ".tsx",
229
+ ".mjs",
230
+ ".cjs",
231
+ ".mts",
232
+ ".cts"
233
+ ],
234
+ tokenPrefilter (source) {
235
+ return source.includes(".css") || source.includes(".scss") || source.includes(".less");
236
+ },
237
+ evaluate ({ helpers, program, relativeFilePath }) {
238
+ const findings = [];
239
+ helpers.walk(program, (node)=>{
240
+ if ("ImportDeclaration" === node.type || "ExportAllDeclaration" === node.type) {
241
+ const sourceValue = helpers.getStringLiteralValue(node.source);
242
+ if (isAllowedImport(relativeFilePath, sourceValue) || !isCssSpecifier(sourceValue)) return;
243
+ findings.push({
244
+ start: node.start,
245
+ end: node.end,
246
+ message: "CSS imports are not allowed here. Route shared styling through approved package entrypoints."
247
+ });
248
+ return;
249
+ }
250
+ if ("ExportNamedDeclaration" === node.type && node.source) {
251
+ const sourceValue = helpers.getStringLiteralValue(node.source);
252
+ if (isAllowedImport(relativeFilePath, sourceValue) || !isCssSpecifier(sourceValue)) return;
253
+ findings.push({
254
+ start: node.start,
255
+ end: node.end,
256
+ message: "CSS re-exports are not allowed here. Route shared styling through approved package entrypoints."
257
+ });
258
+ return;
259
+ }
260
+ if ("CallExpression" === node.type) {
261
+ const calleeName = helpers.getCalleeName(node.callee);
262
+ if ("require" !== calleeName) return;
263
+ const [firstArg] = Array.isArray(node.arguments) ? node.arguments : [];
264
+ const sourceValue = helpers.getStringLiteralValue(firstArg);
265
+ if (isAllowedImport(relativeFilePath, sourceValue) || !isCssSpecifier(sourceValue)) return;
266
+ findings.push({
267
+ start: node.start,
268
+ end: node.end,
269
+ message: "CSS require calls are not allowed here. Route shared styling through approved package entrypoints."
270
+ });
271
+ return;
272
+ }
273
+ if ("ImportExpression" !== node.type) return;
274
+ const sourceValue = helpers.getStringLiteralValue(node.source);
275
+ if (isAllowedImport(relativeFilePath, sourceValue) || !isCssSpecifier(sourceValue)) return;
276
+ findings.push({
277
+ start: node.start,
278
+ end: node.end,
279
+ message: "Dynamic CSS imports are not allowed here. Route shared styling through approved package entrypoints."
280
+ });
281
+ });
282
+ return findings;
283
+ }
284
+ }
285
+ ];
286
+ const EFFECT_HOOKS = new Set([
287
+ "useEffect",
288
+ "useLayoutEffect"
289
+ ]);
290
+ function no_effect_set_state_chain_isNode(value) {
291
+ return Boolean(value) && "object" == typeof value && !Array.isArray(value);
292
+ }
293
+ function isIdentifier(node) {
294
+ return no_effect_set_state_chain_isNode(node) && "Identifier" === node.type && "string" == typeof node.name;
295
+ }
296
+ function getFunctionBody(node) {
297
+ return no_effect_set_state_chain_isNode(node) && no_effect_set_state_chain_isNode(node.body) ? node.body : null;
298
+ }
299
+ function collectStatePairs(body) {
300
+ const setterToState = new Map();
301
+ const statements = Array.isArray(body.body) ? body.body : [];
302
+ for (const statement of statements){
303
+ if (!no_effect_set_state_chain_isNode(statement) || "VariableDeclaration" !== statement.type) continue;
304
+ const declarations = Array.isArray(statement.declarations) ? statement.declarations : [];
305
+ for (const declaration of declarations){
306
+ if (!no_effect_set_state_chain_isNode(declaration)) continue;
307
+ const id = declaration.id;
308
+ const init = declaration.init;
309
+ if (!no_effect_set_state_chain_isNode(id) || !no_effect_set_state_chain_isNode(init) || "CallExpression" !== init.type) continue;
310
+ const callee = init.callee;
311
+ if (!isIdentifier(callee) || "useState" !== callee.name) continue;
312
+ const elements = Array.isArray(id.elements) ? id.elements : [];
313
+ const [stateNode, setterNode] = elements;
314
+ if (!!isIdentifier(stateNode) && !!isIdentifier(setterNode)) setterToState.set(setterNode.name, stateNode.name);
315
+ }
316
+ }
317
+ return setterToState;
318
+ }
319
+ function collectDependencyNames(node) {
320
+ const names = new Set();
321
+ const elements = Array.isArray(node.elements) ? node.elements : [];
322
+ for (const element of elements)if (isIdentifier(element)) names.add(element.name);
323
+ return names;
324
+ }
325
+ function referencesIdentifier(node, identifierName, helpers) {
326
+ let found = false;
327
+ helpers.walk(node, (current)=>{
328
+ if (found) return;
329
+ if ("Identifier" === current.type && current.name === identifierName) found = true;
330
+ });
331
+ return found;
332
+ }
333
+ const createNoEffectSetStateChainGuard = ()=>[
334
+ "guard/no-effect-set-state-chain",
335
+ {
336
+ fileExtensions: [
337
+ ".jsx",
338
+ ".tsx"
339
+ ],
340
+ tokenPrefilter (source) {
341
+ return (source.includes("useEffect") || source.includes("useLayoutEffect")) && source.includes("useState") && source.includes("set");
342
+ },
343
+ evaluate ({ helpers, program }) {
344
+ const findings = [];
345
+ helpers.walk(program, (node)=>{
346
+ if (!helpers.isFunctionLike(node)) return;
347
+ const functionBody = getFunctionBody(node);
348
+ if (!functionBody || "BlockStatement" !== functionBody.type) return;
349
+ const setterToState = collectStatePairs(functionBody);
350
+ if (0 === setterToState.size) return;
351
+ helpers.walk(functionBody, (innerNode)=>{
352
+ if ("CallExpression" !== innerNode.type) return;
353
+ const hookName = helpers.getCalleeName(innerNode.callee);
354
+ if (!hookName || !EFFECT_HOOKS.has(hookName)) return;
355
+ const args = Array.isArray(innerNode.arguments) ? innerNode.arguments : [];
356
+ const callback = args[0];
357
+ const deps = args[1];
358
+ if (!callback || !helpers.isFunctionLike(callback) || !no_effect_set_state_chain_isNode(deps) || "ArrayExpression" !== deps.type) return;
359
+ const depNames = collectDependencyNames(deps);
360
+ if (0 === depNames.size) return;
361
+ const callbackBody = getFunctionBody(callback);
362
+ if (!callbackBody) return;
363
+ helpers.walk(callbackBody, (effectNode)=>{
364
+ if ("CallExpression" !== effectNode.type) return;
365
+ const callee = effectNode.callee;
366
+ if (!isIdentifier(callee)) return;
367
+ const stateName = setterToState.get(callee.name);
368
+ if (!stateName || !depNames.has(stateName)) return;
369
+ const [firstArgument] = Array.isArray(effectNode.arguments) ? effectNode.arguments : [];
370
+ if (!firstArgument || !referencesIdentifier(firstArgument, stateName, helpers)) return;
371
+ findings.push({
372
+ start: effectNode.start,
373
+ end: effectNode.end,
374
+ message: `Avoid deriving state in effects via ${callee.name} from the dependency state "${stateName}".`
375
+ });
376
+ });
377
+ });
378
+ });
379
+ return findings;
380
+ }
381
+ }
382
+ ];
383
+ function no_set_state_in_render_isPascalCase(name) {
384
+ return Boolean(name) && /^[A-Z]/.test(name ?? "");
385
+ }
386
+ function isStateSetterName(name) {
387
+ return Boolean(name) && /^set[A-Z0-9]/.test(name ?? "");
388
+ }
389
+ function scanRenderBody(node, findings) {
390
+ const body = Array.isArray(node.body) ? node.body : [];
391
+ for (const statement of body){
392
+ if (!statement || "object" != typeof statement) continue;
393
+ const current = statement;
394
+ if ("FunctionDeclaration" === current.type || "FunctionExpression" === current.type || "ArrowFunctionExpression" === current.type) continue;
395
+ if ("ExpressionStatement" === current.type) {
396
+ const expression = current.expression;
397
+ if (expression?.type === "CallExpression") {
398
+ const callee = expression.callee;
399
+ if (callee?.type === "Identifier" && isStateSetterName(callee.name)) findings.push({
400
+ start: expression.start,
401
+ end: expression.end,
402
+ message: "Do not call state setters during render. Derive values in render or move the write to an event or effect."
403
+ });
404
+ }
405
+ }
406
+ const consequent = current.consequent;
407
+ if (consequent && Array.isArray(consequent.body)) scanRenderBody(consequent, findings);
408
+ const alternate = current.alternate;
409
+ if (alternate && Array.isArray(alternate.body)) scanRenderBody(alternate, findings);
410
+ if (Array.isArray(current.body)) scanRenderBody(current, findings);
411
+ }
412
+ }
413
+ const createNoSetStateInRenderGuard = ()=>[
414
+ "guard/no-set-state-in-render",
415
+ {
416
+ fileExtensions: [
417
+ ".jsx",
418
+ ".tsx"
419
+ ],
420
+ tokenPrefilter (source) {
421
+ return source.includes("set");
422
+ },
423
+ evaluate ({ helpers, program }) {
424
+ const findings = [];
425
+ helpers.walk(program, (node, parent)=>{
426
+ if (!helpers.isFunctionLike(node)) return;
427
+ const name = helpers.getFunctionName(node, parent);
428
+ if (!no_set_state_in_render_isPascalCase(name)) return;
429
+ const body = node.body;
430
+ if (!body || "BlockStatement" !== body.type) return;
431
+ scanRenderBody(body, findings);
432
+ });
433
+ return findings;
434
+ }
435
+ }
436
+ ];
437
+ const EXCLUDED_PATHS = new Set([
438
+ "packages/guards/",
439
+ "packages/hooks/src/use-local-storage.ts",
440
+ "packages/utils/src/index.ts",
441
+ "scripts/"
442
+ ]);
443
+ function isExcludedPath(relativeFilePath) {
444
+ const normalized = relativeFilePath.split("\\").join("/");
445
+ return [
446
+ ...EXCLUDED_PATHS
447
+ ].some((entry)=>normalized === entry || normalized.startsWith(entry));
448
+ }
449
+ const createNoUnsafeTypeAssertionGuard = ()=>[
450
+ "guard/no-unsafe-type-assertion",
451
+ {
452
+ fileExtensions: [
453
+ ".ts",
454
+ ".tsx",
455
+ ".mts",
456
+ ".cts"
457
+ ],
458
+ tokenPrefilter (source) {
459
+ return source.includes(" as ");
460
+ },
461
+ evaluate ({ helpers, program, relativeFilePath, source }) {
462
+ if (relativeFilePath.endsWith(".d.ts") || isExcludedPath(relativeFilePath)) return [];
463
+ const findings = [];
464
+ helpers.walk(program, (node)=>{
465
+ if ("TSAsExpression" !== node.type) return;
466
+ const expressionText = source.slice(node.start, node.end);
467
+ if (expressionText.trimEnd().endsWith(" as const")) return;
468
+ findings.push({
469
+ start: node.start,
470
+ end: node.end,
471
+ message: "Unsafe type assertions are not allowed here. Prefer narrowing, validation, or better typing."
472
+ });
473
+ });
474
+ return findings;
475
+ }
476
+ }
477
+ ];
478
+ const STORAGE_NAMES = new Set([
479
+ "localStorage",
480
+ "sessionStorage",
481
+ "window.localStorage",
482
+ "window.sessionStorage",
483
+ "globalThis.localStorage",
484
+ "globalThis.sessionStorage"
485
+ ]);
486
+ const ALLOWED_WRAPPER_PATHS = new Set([
487
+ "packages/hooks/src/use-local-storage.ts"
488
+ ]);
489
+ const createNoWebStorageGuard = ()=>[
490
+ "guard/no-web-storage",
491
+ {
492
+ fileExtensions: [
493
+ ".js",
494
+ ".jsx",
495
+ ".ts",
496
+ ".tsx",
497
+ ".mjs",
498
+ ".cjs",
499
+ ".mts",
500
+ ".cts"
501
+ ],
502
+ tokenPrefilter (source) {
503
+ return source.includes("localStorage") || source.includes("sessionStorage");
504
+ },
505
+ evaluate ({ filePath, helpers, program }) {
506
+ const normalizedFilePath = filePath.split("\\").join("/");
507
+ if ([
508
+ ...ALLOWED_WRAPPER_PATHS
509
+ ].some((allowedPath)=>normalizedFilePath.endsWith(allowedPath))) return [];
510
+ const findings = [];
511
+ helpers.walk(program, (node)=>{
512
+ if ("Identifier" !== node.type && "MemberExpression" !== node.type) return;
513
+ const name = "Identifier" === node.type ? helpers.getCalleeName(node) : helpers.getCalleeName(node.object);
514
+ if (!name || !STORAGE_NAMES.has(name)) return;
515
+ findings.push({
516
+ start: node.start,
517
+ end: node.end,
518
+ message: "Direct web storage access is not allowed in ox0 primitives. Use a storage abstraction or inject persistence."
519
+ });
520
+ });
521
+ return findings;
522
+ }
523
+ }
524
+ ];
525
+ const require_effect_cleanup_EFFECT_HOOKS = new Set([
526
+ "useEffect",
527
+ "useLayoutEffect"
528
+ ]);
529
+ const OBSERVER_NAMES = new Set([
530
+ "MutationObserver",
531
+ "ResizeObserver"
532
+ ]);
533
+ function require_effect_cleanup_getFunctionBody(node) {
534
+ const body = node.body;
535
+ return body && "object" == typeof body && !Array.isArray(body) ? body : null;
536
+ }
537
+ function getReturnCleanupFunction(node) {
538
+ const body = require_effect_cleanup_getFunctionBody(node);
539
+ if (!body || "BlockStatement" !== body.type) return null;
540
+ const statements = Array.isArray(body.body) ? body.body : [];
541
+ for (const statement of statements){
542
+ if (!statement || "object" != typeof statement) continue;
543
+ const current = statement;
544
+ if ("ReturnStatement" !== current.type) continue;
545
+ const argument = current.argument;
546
+ if (!!argument && "object" == typeof argument) {
547
+ if ("FunctionExpression" === argument.type || "ArrowFunctionExpression" === argument.type) return argument;
548
+ }
549
+ }
550
+ return null;
551
+ }
552
+ function collectSetups(body, helpers) {
553
+ const setups = [];
554
+ helpers.walk(body, (node)=>{
555
+ if ("CallExpression" === node.type) {
556
+ const calleeName = helpers.getCalleeName(node.callee);
557
+ if ("setInterval" === calleeName || "window.setInterval" === calleeName) setups.push({
558
+ kind: "interval",
559
+ start: node.start,
560
+ end: node.end
561
+ });
562
+ if ("setTimeout" === calleeName || "window.setTimeout" === calleeName) setups.push({
563
+ kind: "timeout",
564
+ start: node.start,
565
+ end: node.end
566
+ });
567
+ if (calleeName?.endsWith(".addEventListener") || "addEventListener" === calleeName) setups.push({
568
+ kind: "event-listener",
569
+ start: node.start,
570
+ end: node.end
571
+ });
572
+ return;
573
+ }
574
+ if ("NewExpression" !== node.type) return;
575
+ const calleeName = helpers.getCalleeName(node.callee);
576
+ if (!calleeName || !OBSERVER_NAMES.has(calleeName)) return;
577
+ setups.push({
578
+ kind: "observer",
579
+ start: node.start,
580
+ end: node.end
581
+ });
582
+ });
583
+ return setups;
584
+ }
585
+ function collectCleanupKinds(cleanupFn, helpers) {
586
+ const kinds = new Set();
587
+ const body = require_effect_cleanup_getFunctionBody(cleanupFn);
588
+ if (!body) return kinds;
589
+ helpers.walk(body, (node)=>{
590
+ if ("CallExpression" !== node.type) return;
591
+ const calleeName = helpers.getCalleeName(node.callee);
592
+ if ("clearInterval" === calleeName || "window.clearInterval" === calleeName) {
593
+ kinds.add("interval");
594
+ return;
595
+ }
596
+ if ("clearTimeout" === calleeName || "window.clearTimeout" === calleeName) {
597
+ kinds.add("timeout");
598
+ return;
599
+ }
600
+ if (calleeName?.endsWith(".removeEventListener") || "removeEventListener" === calleeName) {
601
+ kinds.add("event-listener");
602
+ return;
603
+ }
604
+ if (calleeName?.endsWith(".disconnect") || "disconnect" === calleeName) kinds.add("observer");
605
+ });
606
+ return kinds;
607
+ }
608
+ function getSetupLabel(kind) {
609
+ if ("event-listener" === kind) return "event listeners";
610
+ if ("observer" === kind) return "observers";
611
+ if ("timeout" === kind) return "timeouts";
612
+ return "intervals";
613
+ }
614
+ const createRequireEffectCleanupGuard = ()=>[
615
+ "guard/require-effect-cleanup",
616
+ {
617
+ fileExtensions: [
618
+ ".jsx",
619
+ ".tsx",
620
+ ".ts",
621
+ ".js",
622
+ ".mts",
623
+ ".cts",
624
+ ".mjs",
625
+ ".cjs"
626
+ ],
627
+ tokenPrefilter (source) {
628
+ return (source.includes("useEffect") || source.includes("useLayoutEffect")) && (source.includes("addEventListener") || source.includes("setInterval") || source.includes("setTimeout") || source.includes("MutationObserver") || source.includes("ResizeObserver"));
629
+ },
630
+ evaluate ({ helpers, program }) {
631
+ const findings = [];
632
+ helpers.walk(program, (node)=>{
633
+ if ("CallExpression" !== node.type) return;
634
+ const hookName = helpers.getCalleeName(node.callee);
635
+ if (!hookName || !require_effect_cleanup_EFFECT_HOOKS.has(hookName)) return;
636
+ const [callback] = Array.isArray(node.arguments) ? node.arguments : [];
637
+ if (!callback || "object" != typeof callback || !helpers.isFunctionLike(callback)) return;
638
+ const callbackBody = require_effect_cleanup_getFunctionBody(callback);
639
+ if (!callbackBody) return;
640
+ const setups = collectSetups(callbackBody, helpers);
641
+ if (0 === setups.length) return;
642
+ const cleanupFn = getReturnCleanupFunction(callback);
643
+ const cleanupKinds = cleanupFn ? collectCleanupKinds(cleanupFn, helpers) : new Set();
644
+ for (const setup of setups)if (!cleanupKinds.has(setup.kind)) findings.push({
645
+ start: setup.start,
646
+ end: setup.end,
647
+ message: `Effects that create ${getSetupLabel(setup.kind)} must return a matching cleanup.`
648
+ });
649
+ });
650
+ return findings;
651
+ }
652
+ }
653
+ ];
654
+ function isInlineUnstableExpression(node) {
655
+ return "ObjectExpression" === node.type || "ArrayExpression" === node.type || "ArrowFunctionExpression" === node.type || "FunctionExpression" === node.type || "NewExpression" === node.type;
656
+ }
657
+ const createStableProviderValuesGuard = ()=>[
658
+ "guard/stable-provider-values",
659
+ {
660
+ fileExtensions: [
661
+ ".jsx",
662
+ ".tsx"
663
+ ],
664
+ tokenPrefilter (source) {
665
+ return source.includes("Provider") && source.includes("value=");
666
+ },
667
+ evaluate ({ helpers, program }) {
668
+ const findings = [];
669
+ helpers.walk(program, (node, parent)=>{
670
+ if ("JSXAttribute" !== node.type) return;
671
+ const attributeName = helpers.getJsxElementName(node.name);
672
+ if ("value" !== attributeName) return;
673
+ if (!parent || "JSXOpeningElement" !== parent.type || !helpers.getJsxElementName(parent.name)?.endsWith("Provider")) return;
674
+ const valueNode = node.value;
675
+ if (!valueNode || "JSXExpressionContainer" !== valueNode.type) return;
676
+ const expression = valueNode.expression;
677
+ if (!expression || "object" != typeof expression) return;
678
+ if (!isInlineUnstableExpression(expression)) return;
679
+ findings.push({
680
+ start: node.start,
681
+ end: node.end,
682
+ message: "Provider values should be referentially stable. Hoist or memoize the value before passing it to a Provider."
683
+ });
684
+ });
685
+ return findings;
686
+ }
687
+ }
688
+ ];
689
+ const GUARD_FACTORIES = [
690
+ createNoAsyncReactComponentGuard,
691
+ createNoBareIntlGuard,
692
+ createNoCssImportGuard,
693
+ createNoEffectSetStateChainGuard,
694
+ createNoSetStateInRenderGuard,
695
+ createNoUnsafeTypeAssertionGuard,
696
+ createStableProviderValuesGuard,
697
+ createNoWebStorageGuard,
698
+ createMissingLabelGuard,
699
+ createRequireEffectCleanupGuard
700
+ ];
701
+ const INTERNAL_INVALID_DISABLE_RULE = "guard/invalid-disable-comment";
702
+ const INTERNAL_PARSE_ERROR_RULE = "guard/parse-error";
703
+ const SCRIPT_EXTENSIONS = new Set([
704
+ ".js",
705
+ ".jsx",
706
+ ".ts",
707
+ ".tsx",
708
+ ".mjs",
709
+ ".cjs",
710
+ ".mts",
711
+ ".cts"
712
+ ]);
713
+ const EXCLUDED_DIRECTORIES = new Set([
714
+ ".git",
715
+ ".idea",
716
+ ".next",
717
+ ".rspress",
718
+ ".turbo",
719
+ "coverage",
720
+ "dist",
721
+ "doc_build",
722
+ "node_modules"
723
+ ]);
724
+ function isScriptFile(filePath) {
725
+ return SCRIPT_EXTENSIONS.has(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].extname(filePath));
726
+ }
727
+ function shouldExcludeDirectory(relativeDirPath) {
728
+ return relativeDirPath.split(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].sep).some((segment)=>segment.length > 0 && EXCLUDED_DIRECTORIES.has(segment));
729
+ }
730
+ function collectProjectFiles(projectRoot, allowedExtensions) {
731
+ const files = [];
732
+ function walkDirectory(currentDir) {
733
+ const entries = __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__["default"].readdirSync(currentDir, {
734
+ withFileTypes: true
735
+ });
736
+ for (const entry of entries){
737
+ const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(currentDir, entry.name);
738
+ const relativePath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].relative(projectRoot, fullPath);
739
+ if (entry.isDirectory()) {
740
+ if (shouldExcludeDirectory(relativePath)) continue;
741
+ walkDirectory(fullPath);
742
+ continue;
743
+ }
744
+ if (allowedExtensions.has(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].extname(entry.name))) files.push(fullPath);
745
+ }
746
+ }
747
+ walkDirectory(projectRoot);
748
+ return files.toSorted();
749
+ }
750
+ function createLineStarts(source) {
751
+ const starts = [
752
+ 0
753
+ ];
754
+ for(let index = 0; index < source.length; index += 1)if ("\n" === source[index]) starts.push(index + 1);
755
+ return starts;
756
+ }
757
+ function getLocationFromOffset(lineStarts, offset) {
758
+ let low = 0;
759
+ let high = lineStarts.length - 1;
760
+ while(low <= high){
761
+ const mid = Math.floor((low + high) / 2);
762
+ const start = lineStarts[mid];
763
+ const nextStart = lineStarts[mid + 1] ?? Number.POSITIVE_INFINITY;
764
+ if (offset < start) high = mid - 1;
765
+ else {
766
+ if (!(offset >= nextStart)) return {
767
+ line: mid + 1,
768
+ column: offset - lineStarts[mid] + 1
769
+ };
770
+ low = mid + 1;
771
+ }
772
+ }
773
+ return {
774
+ line: 1,
775
+ column: 1
776
+ };
777
+ }
778
+ function buildFinding(ruleId, rawFinding, filePath, relativeFilePath, lineStarts) {
779
+ const startLocation = getLocationFromOffset(lineStarts, rawFinding.start);
780
+ const endLocation = getLocationFromOffset(lineStarts, rawFinding.end ?? rawFinding.start);
781
+ return {
782
+ ruleId,
783
+ message: rawFinding.message,
784
+ filePath,
785
+ relativeFilePath,
786
+ line: startLocation.line,
787
+ column: startLocation.column,
788
+ endLine: endLocation.line,
789
+ endColumn: endLocation.column,
790
+ severity: "error"
791
+ };
792
+ }
793
+ function parseDisableDirective(line, lineNumber) {
794
+ const match = line.match(/^\s*\/\/\s*guard-disable-(next-line|start|end)\s+(\S+)(?:\s+(.*\S))?\s*$/);
795
+ if (!match) return null;
796
+ return {
797
+ type: match[1],
798
+ ruleId: match[2],
799
+ rationale: match[3] ?? "",
800
+ line: lineNumber,
801
+ column: line.indexOf("//") + 1
802
+ };
803
+ }
804
+ function parseSuppressions(source, knownRuleIds) {
805
+ const suppressions = [];
806
+ const invalid = [];
807
+ const openBlocks = new Map();
808
+ const lines = source.split(/\r?\n/);
809
+ for(let index = 0; index < lines.length; index += 1){
810
+ const lineNumber = index + 1;
811
+ const directive = parseDisableDirective(lines[index], lineNumber);
812
+ if (!directive) continue;
813
+ if (!knownRuleIds.has(directive.ruleId)) {
814
+ invalid.push({
815
+ line: directive.line,
816
+ column: directive.column,
817
+ message: `Disable comment references unknown rule "${directive.ruleId}".`
818
+ });
819
+ continue;
820
+ }
821
+ if (("next-line" === directive.type || "start" === directive.type) && "" === directive.rationale.trim()) {
822
+ invalid.push({
823
+ line: directive.line,
824
+ column: directive.column,
825
+ message: `Disable comment for ${directive.ruleId} must include a rationale.`
826
+ });
827
+ continue;
828
+ }
829
+ if ("next-line" === directive.type) {
830
+ suppressions.push({
831
+ ruleId: directive.ruleId,
832
+ startLine: directive.line + 1,
833
+ endLine: directive.line + 1
834
+ });
835
+ continue;
836
+ }
837
+ if ("start" === directive.type) {
838
+ openBlocks.set(directive.ruleId, directive);
839
+ continue;
840
+ }
841
+ const startDirective = openBlocks.get(directive.ruleId);
842
+ if (!startDirective) {
843
+ invalid.push({
844
+ line: directive.line,
845
+ column: directive.column,
846
+ message: `guard-disable-end for ${directive.ruleId} has no matching start.`
847
+ });
848
+ continue;
849
+ }
850
+ suppressions.push({
851
+ ruleId: directive.ruleId,
852
+ startLine: startDirective.line + 1,
853
+ endLine: Math.max(startDirective.line + 1, directive.line - 1)
854
+ });
855
+ openBlocks.delete(directive.ruleId);
856
+ }
857
+ for (const directive of openBlocks.values())invalid.push({
858
+ line: directive.line,
859
+ column: directive.column,
860
+ message: `guard-disable-start for ${directive.ruleId} is missing a matching end.`
861
+ });
862
+ return {
863
+ suppressions,
864
+ invalid
865
+ };
866
+ }
867
+ function isSuppressed(finding, suppressions) {
868
+ return suppressions.some((suppression)=>suppression.ruleId === finding.ruleId && finding.line >= suppression.startLine && finding.line <= suppression.endLine);
869
+ }
870
+ function sortFindings(findings) {
871
+ const sortedFindings = findings.toSorted((left, right)=>{
872
+ if (left.relativeFilePath !== right.relativeFilePath) return left.relativeFilePath.localeCompare(right.relativeFilePath);
873
+ if (left.line !== right.line) return left.line - right.line;
874
+ if (left.column !== right.column) return left.column - right.column;
875
+ return left.ruleId.localeCompare(right.ruleId);
876
+ });
877
+ findings.splice(0, findings.length, ...sortedFindings);
878
+ }
879
+ function getRuleRegistry() {
880
+ const registry = new Map();
881
+ for (const factory of GUARD_FACTORIES){
882
+ const [ruleId, definition] = factory();
883
+ registry.set(ruleId, definition);
884
+ }
885
+ return registry;
886
+ }
887
+ function listGuardRuleIds() {
888
+ return [
889
+ ...getRuleRegistry().keys()
890
+ ].toSorted();
891
+ }
892
+ function runGuards(options) {
893
+ const projectRoot = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].resolve(options.cwd ?? process.cwd(), options.projectRoot);
894
+ const registry = getRuleRegistry();
895
+ const knownRuleIds = new Set([
896
+ ...registry.keys(),
897
+ INTERNAL_INVALID_DISABLE_RULE,
898
+ INTERNAL_PARSE_ERROR_RULE
899
+ ]);
900
+ const requestedRuleIds = options.requestedRules?.length ? options.requestedRules.map((ruleId)=>ruleId) : [
901
+ ...registry.keys()
902
+ ];
903
+ for (const ruleId of requestedRuleIds)if (!registry.has(ruleId)) throw new Error(`Unknown guard rule: ${ruleId}`);
904
+ const selectedEntries = [
905
+ ...registry.entries()
906
+ ].filter(([ruleId])=>requestedRuleIds.includes(ruleId));
907
+ const allowedExtensions = new Set();
908
+ for (const [, definition] of selectedEntries)for (const extension of definition.fileExtensions)allowedExtensions.add(extension);
909
+ const files = collectProjectFiles(projectRoot, allowedExtensions);
910
+ const findings = [];
911
+ for (const filePath of files){
912
+ const relativeFilePath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].relative(projectRoot, filePath);
913
+ const source = __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__["default"].readFileSync(filePath, "utf8");
914
+ const lineStarts = createLineStarts(source);
915
+ const { suppressions, invalid } = parseSuppressions(source, knownRuleIds);
916
+ for (const invalidFinding of invalid)findings.push({
917
+ ruleId: INTERNAL_INVALID_DISABLE_RULE,
918
+ message: invalidFinding.message,
919
+ filePath,
920
+ relativeFilePath,
921
+ line: invalidFinding.line,
922
+ column: invalidFinding.column,
923
+ endLine: invalidFinding.line,
924
+ endColumn: invalidFinding.column,
925
+ severity: "error"
926
+ });
927
+ const extension = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].extname(filePath);
928
+ const applicableRules = selectedEntries.filter(([, definition])=>definition.fileExtensions.includes(extension) && definition.tokenPrefilter(source));
929
+ if (0 === applicableRules.length) continue;
930
+ for (const [ruleId, definition] of applicableRules.filter(([, candidate])=>"function" == typeof candidate.evaluateText)){
931
+ const rawFindings = definition.evaluateText?.({
932
+ ruleId,
933
+ projectRoot,
934
+ filePath,
935
+ relativeFilePath,
936
+ source
937
+ }) ?? [];
938
+ for (const rawFinding of rawFindings){
939
+ const finding = buildFinding(ruleId, rawFinding, filePath, relativeFilePath, lineStarts);
940
+ if (!isSuppressed(finding, suppressions)) findings.push(finding);
941
+ }
942
+ }
943
+ const astRules = applicableRules.filter(([, definition])=>"function" == typeof definition.evaluate && isScriptFile(filePath));
944
+ if (0 === astRules.length) continue;
945
+ const parseResult = (0, __WEBPACK_EXTERNAL_MODULE_oxc_parser_1c05de9b__.parseSync)(relativeFilePath, source, {
946
+ astType: "ts"
947
+ });
948
+ if (Array.isArray(parseResult.errors) && parseResult.errors.length > 0) {
949
+ for (const error of parseResult.errors){
950
+ const start = "start" in error && "number" == typeof error.start ? error.start : 0;
951
+ findings.push(buildFinding(INTERNAL_PARSE_ERROR_RULE, {
952
+ start,
953
+ end: start,
954
+ message: `Parse error: ${String(error.message ?? "unknown parser error")}`
955
+ }, filePath, relativeFilePath, lineStarts));
956
+ }
957
+ continue;
958
+ }
959
+ for (const [ruleId, definition] of astRules){
960
+ const rawFindings = definition.evaluate?.({
961
+ ruleId,
962
+ projectRoot,
963
+ filePath,
964
+ relativeFilePath,
965
+ source,
966
+ helpers: GUARD_HELPERS,
967
+ program: parseResult.program,
968
+ comments: parseResult.comments ?? []
969
+ }) ?? [];
970
+ for (const rawFinding of rawFindings){
971
+ const finding = buildFinding(ruleId, rawFinding, filePath, relativeFilePath, lineStarts);
972
+ if (!isSuppressed(finding, suppressions)) findings.push(finding);
973
+ }
974
+ }
975
+ }
976
+ sortFindings(findings);
977
+ return {
978
+ findings,
979
+ ruleIds: listGuardRuleIds()
980
+ };
981
+ }
982
+ function defineGuardsConfig(config) {
983
+ return config;
984
+ }
985
+ const recommendedRules = [
986
+ {
987
+ id: "guard/no-async-react-component",
988
+ severity: "error"
989
+ },
990
+ {
991
+ id: "guard/no-css-import",
992
+ severity: "error"
993
+ },
994
+ {
995
+ id: "guard/no-effect-set-state-chain",
996
+ severity: "error"
997
+ },
998
+ {
999
+ id: "guard/no-set-state-in-render",
1000
+ severity: "error"
1001
+ },
1002
+ {
1003
+ id: "guard/no-unsafe-type-assertion",
1004
+ severity: "error"
1005
+ },
1006
+ {
1007
+ id: "guard/stable-provider-values",
1008
+ severity: "error"
1009
+ },
1010
+ {
1011
+ id: "guard/no-web-storage",
1012
+ severity: "error"
1013
+ },
1014
+ {
1015
+ id: "guard/missing-label",
1016
+ severity: "error"
1017
+ },
1018
+ {
1019
+ id: "guard/require-effect-cleanup",
1020
+ severity: "error"
1021
+ }
1022
+ ];
1023
+ export { GUARD_FACTORIES, defineGuardsConfig, listGuardRuleIds, recommendedRules, runGuards };