@qds.dev/tools 0.11.2 → 0.13.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.
Files changed (38) hide show
  1. package/lib/linter/qds-internal.d.ts +204 -1
  2. package/lib/linter/qds.d.ts +59 -0
  3. package/lib/linter/qds.unit.d.ts +1 -0
  4. package/lib/linter/rule-tester.d.ts +23 -1
  5. package/lib/playground/prop-extraction.d.ts +6 -1
  6. package/lib/playground/prop-extraction.qwik.mjs +68 -9
  7. package/lib/playground/scenario-injection.qwik.mjs +41 -8
  8. package/lib/rolldown/as-child.d.ts +6 -5
  9. package/lib/rolldown/as-child.qwik.mjs +52 -91
  10. package/lib/rolldown/index.d.ts +3 -2
  11. package/lib/rolldown/index.qwik.mjs +2 -3
  12. package/lib/rolldown/inject-component-types.qwik.mjs +1 -1
  13. package/lib/rolldown/inline-asset.qwik.mjs +6 -6
  14. package/lib/rolldown/inline-css.qwik.mjs +1 -1
  15. package/lib/rolldown/qds-types.d.ts +41 -0
  16. package/lib/rolldown/qds.d.ts +5 -0
  17. package/lib/rolldown/qds.qwik.mjs +147 -0
  18. package/lib/rolldown/qds.unit.d.ts +1 -0
  19. package/lib/rolldown/ui-types.d.ts +42 -0
  20. package/lib/rolldown/ui.d.ts +12 -0
  21. package/lib/rolldown/ui.qwik.mjs +445 -0
  22. package/lib/rolldown/ui.unit.d.ts +1 -0
  23. package/lib/utils/icons/transform/mdx.d.ts +3 -11
  24. package/lib/utils/icons/transform/mdx.qwik.mjs +14 -20
  25. package/lib/utils/icons/transform/tsx.d.ts +3 -12
  26. package/lib/utils/icons/transform/tsx.qwik.mjs +28 -37
  27. package/lib/utils/index.qwik.mjs +5 -5
  28. package/lib/utils/transform-dts.qwik.mjs +1 -1
  29. package/lib/vite/index.d.ts +2 -2
  30. package/lib/vite/index.qwik.mjs +2 -3
  31. package/lib/vite/minify-content.qwik.mjs +1 -1
  32. package/linter/qds-internal.ts +707 -0
  33. package/linter/qds-internal.unit.ts +399 -0
  34. package/linter/qds.ts +300 -0
  35. package/linter/qds.unit.ts +158 -0
  36. package/linter/rule-tester.ts +395 -0
  37. package/package.json +8 -7
  38. package/lib/rolldown/icons.qwik.mjs +0 -107
@@ -0,0 +1,707 @@
1
+ /**
2
+ * Oxlint plugin for Qwik Design System
3
+ *
4
+ * Custom linting rules specific to Qwik patterns and conventions
5
+ */
6
+
7
+ interface Ranged {
8
+ range: [number, number];
9
+ }
10
+
11
+ interface Node extends Ranged {
12
+ type: string;
13
+ start: number;
14
+ end: number;
15
+ }
16
+
17
+ interface Context {
18
+ filename: string;
19
+ report(descriptor: {
20
+ node: Ranged;
21
+ messageId?: string;
22
+ message?: string;
23
+ data?: Record<string, string>;
24
+ }): void;
25
+ }
26
+
27
+ namespace ESTree {
28
+ interface BaseNode extends Ranged {
29
+ type: string;
30
+ [key: string]: unknown;
31
+ }
32
+ export interface Identifier extends BaseNode {
33
+ type: "Identifier";
34
+ name: string;
35
+ }
36
+ export interface TSPropertySignature extends BaseNode {
37
+ type: "TSPropertySignature";
38
+ key: Identifier | BaseNode;
39
+ computed: boolean;
40
+ optional: boolean;
41
+ readonly: boolean;
42
+ }
43
+ export interface JSXIdentifier extends BaseNode {
44
+ type: "JSXIdentifier";
45
+ name: string;
46
+ }
47
+ export interface JSXExpressionContainer extends BaseNode {
48
+ type: "JSXExpressionContainer";
49
+ expression: Expression;
50
+ }
51
+ export interface JSXAttribute extends BaseNode {
52
+ type: "JSXAttribute";
53
+ name: JSXIdentifier | BaseNode;
54
+ value: JSXExpressionContainer | BaseNode | null;
55
+ }
56
+ export interface CallExpression extends BaseNode {
57
+ type: "CallExpression";
58
+ callee: Expression;
59
+ arguments: BaseNode[];
60
+ }
61
+ export interface VariableDeclarator extends BaseNode {
62
+ type: "VariableDeclarator";
63
+ id: Identifier | BaseNode;
64
+ init: Expression | null;
65
+ }
66
+ export interface ReturnStatement extends BaseNode {
67
+ type: "ReturnStatement";
68
+ argument: Expression | null;
69
+ }
70
+ export interface VariableDeclaration extends BaseNode {
71
+ type: "VariableDeclaration";
72
+ declarations: VariableDeclarator[];
73
+ }
74
+ export type Expression = BaseNode;
75
+ export type JSXElement = BaseNode;
76
+ }
77
+
78
+ function defineRule<const T extends Record<string, unknown>>(rule: T): T {
79
+ return rule;
80
+ }
81
+ function definePlugin<const T extends Record<string, unknown>>(plugin: T): T {
82
+ return plugin;
83
+ }
84
+
85
+ const noDefaultXNamingRule = defineRule({
86
+ meta: {
87
+ type: "problem",
88
+ docs: {
89
+ description:
90
+ "Disallow 'defaultX' naming pattern in component props - use Qwik's signal/value-based patterns instead",
91
+ category: "Best Practices",
92
+ recommended: true,
93
+ url: "https://qds.dev/contributing/state/"
94
+ },
95
+ messages: {
96
+ defaultXPattern:
97
+ "'{{name}}' uses React's defaultX pattern. Qwik uses signal-based (two-way binding) or value-based (one-way binding) patterns instead. See: https://qds.dev/contributing/state/"
98
+ },
99
+ schema: []
100
+ },
101
+
102
+ createOnce(context: Context) {
103
+ const defaultPattern = /^default[A-Z][a-zA-Z0-9]*$/;
104
+
105
+ return {
106
+ TSPropertySignature(node: Node) {
107
+ const tsNode = node as unknown as ESTree.TSPropertySignature;
108
+ if (tsNode.key.type !== "Identifier") return;
109
+ const key = tsNode.key as ESTree.Identifier;
110
+ if (!key.name) return;
111
+
112
+ const name = key.name;
113
+ if (!defaultPattern.test(name)) return;
114
+
115
+ context.report({
116
+ node: key,
117
+ messageId: "defaultXPattern",
118
+ data: { name }
119
+ });
120
+ }
121
+ };
122
+ }
123
+ });
124
+
125
+ const eventHandlersArrayPatternRule = defineRule({
126
+ meta: {
127
+ type: "problem",
128
+ docs: {
129
+ description:
130
+ "Event handlers should use array pattern to combine local handlers with props",
131
+ category: "Best Practices",
132
+ recommended: true,
133
+ url: "https://qds.dev/contributing/"
134
+ },
135
+ messages: {
136
+ requireArrayPattern:
137
+ "Event handler '{{name}}' should use array pattern: [localHandler$, props.{{name}}]."
138
+ },
139
+ schema: []
140
+ },
141
+
142
+ createOnce(context: Context) {
143
+ return {
144
+ JSXAttribute(node: Node) {
145
+ const jsxAttr = node as unknown as ESTree.JSXAttribute;
146
+
147
+ if (jsxAttr.name.type !== "JSXIdentifier") return;
148
+
149
+ const attrName = (jsxAttr.name as ESTree.JSXIdentifier).name;
150
+ if (!attrName.startsWith("on") || !attrName.endsWith("$")) return;
151
+
152
+ const value = jsxAttr.value;
153
+ if (!value || value.type !== "JSXExpressionContainer") return;
154
+
155
+ const expr = (value as ESTree.JSXExpressionContainer).expression;
156
+
157
+ // Allow array expressions: onXxx$={[handler$, props.onXxx$]}
158
+ if (expr.type === "ArrayExpression") return;
159
+
160
+ // Allow ternary expressions (single-line ternaries are valid)
161
+ if (expr.type === "ConditionalExpression") return;
162
+
163
+ context.report({
164
+ node: jsxAttr.name,
165
+ messageId: "requireArrayPattern",
166
+ data: { name: attrName }
167
+ });
168
+ }
169
+ };
170
+ }
171
+ });
172
+
173
+ function collectHTMLElements(node: ESTree.Expression): string[] {
174
+ const elements: string[] = [];
175
+
176
+ const walk = (n: unknown): void => {
177
+ if (!n || typeof n !== "object") return;
178
+
179
+ const record = n as Record<string, unknown>;
180
+ if (record.type === "JSXElement") {
181
+ const openingElement = record.openingElement as Record<string, unknown> | undefined;
182
+ if (openingElement?.name) {
183
+ const name = openingElement.name as Record<string, unknown>;
184
+ if (name.type === "JSXIdentifier") {
185
+ const tagName = name.name as string;
186
+ // Only collect lowercase tags (HTML elements, not components)
187
+ if (tagName && tagName[0] === tagName[0].toLowerCase()) {
188
+ elements.push(tagName);
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // Recursively walk children
195
+ for (const key in record) {
196
+ const value = record[key];
197
+ if (key === "children" && Array.isArray(value)) {
198
+ value.forEach(walk);
199
+ } else if (value && typeof value === "object") {
200
+ walk(value);
201
+ }
202
+ }
203
+ };
204
+
205
+ walk(node);
206
+ return elements;
207
+ }
208
+
209
+ const oneElementCompositionRule = defineRule({
210
+ meta: {
211
+ type: "problem",
212
+ docs: {
213
+ description:
214
+ "Components should follow 'One Component, One Markup Element' principle",
215
+ category: "Best Practices",
216
+ recommended: true,
217
+ url: "https://qds.dev/contributing/composition/"
218
+ },
219
+ messages: {
220
+ multipleElements:
221
+ "Component returns multiple HTML element types: {{elements}}. Each component should correspond to ONE type of markup element. Add '// no-composition-check' to exempt."
222
+ },
223
+ schema: []
224
+ },
225
+
226
+ createOnce(context: Context) {
227
+ let hasCompositionCheckDisable = false;
228
+ const componentReturnElements = new Map<string, Set<string>>();
229
+ let currentComponentName: string | null = null;
230
+
231
+ return {
232
+ Program() {
233
+ // Check for no-composition-check comment
234
+ // Note: In a real implementation, we'd check context.sourceCode.getAllComments()
235
+ // For now, we'll skip this feature in the linter
236
+ hasCompositionCheckDisable = false;
237
+ },
238
+
239
+ VariableDeclarator(node: Node) {
240
+ const decl = node as unknown as ESTree.VariableDeclarator;
241
+ if (decl.init?.type !== "CallExpression") return;
242
+
243
+ const callee = (decl.init as ESTree.CallExpression).callee as ESTree.Identifier;
244
+ if (callee.type !== "Identifier") return;
245
+ if (callee.name !== "component$") return;
246
+ if (decl.id.type !== "Identifier") return;
247
+
248
+ currentComponentName = (decl.id as ESTree.Identifier).name;
249
+ componentReturnElements.set(currentComponentName, new Set());
250
+ },
251
+
252
+ ReturnStatement(node: Node) {
253
+ if (!currentComponentName || hasCompositionCheckDisable) return;
254
+
255
+ const returnStmt = node as unknown as ESTree.ReturnStatement;
256
+ if (!returnStmt.argument) return;
257
+
258
+ // Collect HTML elements from the return statement
259
+ const elements = collectHTMLElements(returnStmt.argument);
260
+ const elementSet = componentReturnElements.get(currentComponentName);
261
+
262
+ if (elementSet) {
263
+ elements.forEach((el) => elementSet.add(el));
264
+ }
265
+ },
266
+
267
+ "VariableDeclarator:exit"(node: Node) {
268
+ const decl = node as unknown as ESTree.VariableDeclarator;
269
+ if (decl.init?.type !== "CallExpression") return;
270
+
271
+ const callee = (decl.init as ESTree.CallExpression).callee as ESTree.Identifier;
272
+ if (callee.type !== "Identifier") return;
273
+ if (callee.name !== "component$") return;
274
+ if (!currentComponentName) return;
275
+ if (decl.id.type !== "Identifier") return;
276
+
277
+ const elementSet = componentReturnElements.get(currentComponentName);
278
+
279
+ if (elementSet && elementSet.size > 1) {
280
+ const elements = Array.from(elementSet).toSorted().join(", ");
281
+ context.report({
282
+ node: decl.id,
283
+ messageId: "multipleElements",
284
+ data: { elements }
285
+ });
286
+ }
287
+
288
+ currentComponentName = null;
289
+ }
290
+ };
291
+ }
292
+ });
293
+
294
+ const requireUseBindingsRule = defineRule({
295
+ meta: {
296
+ type: "problem",
297
+ docs: {
298
+ description:
299
+ "Root components (*-root.tsx) should use useBindings or include '// no-bindings' comment",
300
+ category: "Best Practices",
301
+ recommended: true,
302
+ url: "https://qds.dev/contributing/state/#useBindings"
303
+ },
304
+ messages: {
305
+ missingBindings:
306
+ "Root component is missing useBindings. Add useBindings or include '// no-bindings' comment if not needed."
307
+ },
308
+ schema: []
309
+ },
310
+
311
+ createOnce(context: Context) {
312
+ let hasUseBindings = false;
313
+ let hasNoBindingsComment = false;
314
+ let hasComponent = false;
315
+ let componentNode: Ranged | null = null;
316
+ let isRootComponent = false;
317
+
318
+ return {
319
+ Program() {
320
+ // Check if this is a root component file
321
+ const filename = context.filename;
322
+ isRootComponent =
323
+ filename.endsWith("-root.tsx") || filename.endsWith("-root.jsx");
324
+
325
+ // Note: In a real implementation, we'd check for comments here
326
+ hasNoBindingsComment = false;
327
+ },
328
+
329
+ CallExpression(node: Node) {
330
+ if (!isRootComponent) return;
331
+
332
+ const call = node as unknown as ESTree.CallExpression;
333
+ const callee = call.callee;
334
+ if (callee.type !== "Identifier") return;
335
+
336
+ // Check for component$
337
+ if (callee.name === "component$") {
338
+ hasComponent = true;
339
+ componentNode = node;
340
+ }
341
+
342
+ // Check for useBindings
343
+ if (callee.name === "useBindings") {
344
+ hasUseBindings = true;
345
+ }
346
+ },
347
+
348
+ "Program:exit"() {
349
+ if (!isRootComponent) return;
350
+ if (!hasComponent) return;
351
+ if (hasUseBindings) return;
352
+ if (hasNoBindingsComment) return;
353
+ if (!componentNode) return;
354
+
355
+ context.report({
356
+ node: componentNode,
357
+ messageId: "missingBindings"
358
+ });
359
+ }
360
+ };
361
+ }
362
+ });
363
+
364
+ const requireResearchFileRule = defineRule({
365
+ meta: {
366
+ type: "problem",
367
+ docs: {
368
+ description:
369
+ "Root components (*-root.tsx) require a research file (research.md or research.mdx)",
370
+ category: "Best Practices",
371
+ recommended: true,
372
+ url: "https://qwik.design/contributing/research/"
373
+ },
374
+ messages: {
375
+ missingResearch:
376
+ "Root component requires a research file. Create research.md or research.mdx in this directory documenting component research, accessibility, design decisions, and usage guidelines."
377
+ },
378
+ schema: []
379
+ },
380
+
381
+ createOnce(context: Context) {
382
+ let isRootComponent = false;
383
+ let hasComponent = false;
384
+ let componentNode: Ranged | null = null;
385
+
386
+ return {
387
+ Program() {
388
+ const filename = context.filename;
389
+ isRootComponent =
390
+ filename.endsWith("-root.tsx") || filename.endsWith("-root.jsx");
391
+ },
392
+
393
+ CallExpression(node: Node) {
394
+ if (!isRootComponent) return;
395
+
396
+ const call = node as unknown as ESTree.CallExpression;
397
+ const callee = call.callee;
398
+ if (callee.type !== "Identifier") return;
399
+
400
+ if (callee.name === "component$") {
401
+ hasComponent = true;
402
+ componentNode = node;
403
+ }
404
+ },
405
+
406
+ "Program:exit"() {
407
+ if (!isRootComponent) return;
408
+ if (!hasComponent) return;
409
+ if (!componentNode) return;
410
+
411
+ // Note: We can't actually check the filesystem from oxlint
412
+ // This rule serves as a reminder/documentation
413
+ // The actual enforcement is done by the GitHub Actions workflow
414
+ context.report({
415
+ node: componentNode,
416
+ messageId: "missingResearch"
417
+ });
418
+ }
419
+ };
420
+ }
421
+ });
422
+
423
+ const requireTestFileRule = defineRule({
424
+ meta: {
425
+ type: "problem",
426
+ docs: {
427
+ description: "Root components (*-root.tsx) require a test file (*.browser.tsx)",
428
+ category: "Best Practices",
429
+ recommended: true,
430
+ url: "https://qwik.design/contributing/testing/"
431
+ },
432
+ messages: {
433
+ missingTest:
434
+ "Root component requires a test file. Create a *.browser.tsx file in this directory with component tests."
435
+ },
436
+ schema: []
437
+ },
438
+
439
+ createOnce(context: Context) {
440
+ let isRootComponent = false;
441
+ let hasComponent = false;
442
+ let componentNode: Ranged | null = null;
443
+
444
+ return {
445
+ Program() {
446
+ const filename = context.filename;
447
+ isRootComponent =
448
+ filename.endsWith("-root.tsx") || filename.endsWith("-root.jsx");
449
+ },
450
+
451
+ CallExpression(node: Node) {
452
+ if (!isRootComponent) return;
453
+
454
+ const call = node as unknown as ESTree.CallExpression;
455
+ const callee = call.callee;
456
+ if (callee.type !== "Identifier") return;
457
+
458
+ if (callee.name === "component$") {
459
+ hasComponent = true;
460
+ componentNode = node;
461
+ }
462
+ },
463
+
464
+ "Program:exit"() {
465
+ if (!isRootComponent) return;
466
+ if (!hasComponent) return;
467
+ if (!componentNode) return;
468
+
469
+ // Note: We can't actually check the filesystem from oxlint
470
+ // This rule serves as a reminder/documentation
471
+ // The actual enforcement is done by the GitHub Actions workflow
472
+ context.report({
473
+ node: componentNode,
474
+ messageId: "missingTest"
475
+ });
476
+ }
477
+ };
478
+ }
479
+ });
480
+
481
+ const requireDestructureBindingsRule = defineRule({
482
+ meta: {
483
+ type: "problem",
484
+ docs: {
485
+ description:
486
+ "If useBindings is used, destructureBindings must also be used in the same file to ensure proper prop handling",
487
+ category: "Best Practices",
488
+ recommended: true,
489
+ url: "https://qds.dev/contributing/state/#destructureBindings"
490
+ },
491
+ messages: {
492
+ missingDestructureBindings:
493
+ "File uses useBindings but is missing destructureBindings. Both are required for proper QDS component state management. Example: const { ... } = destructureBindings(props, initialValues);"
494
+ },
495
+ schema: []
496
+ },
497
+
498
+ createOnce(context: Context) {
499
+ let hasUseBindings = false;
500
+ let hasDestructureBindings = false;
501
+ let useBindingsNode: Ranged | null = null;
502
+
503
+ return {
504
+ CallExpression(node: Node) {
505
+ const call = node as unknown as ESTree.CallExpression;
506
+ const callee = call.callee;
507
+ if (callee.type !== "Identifier") return;
508
+
509
+ if (callee.name === "useBindings") {
510
+ hasUseBindings = true;
511
+ useBindingsNode = node;
512
+ }
513
+
514
+ if (callee.name === "destructureBindings") {
515
+ hasDestructureBindings = true;
516
+ }
517
+ },
518
+
519
+ "Program:exit"() {
520
+ if (hasUseBindings && !hasDestructureBindings && useBindingsNode) {
521
+ context.report({
522
+ node: useBindingsNode,
523
+ messageId: "missingDestructureBindings"
524
+ });
525
+ }
526
+ }
527
+ };
528
+ }
529
+ });
530
+
531
+ export const requireContextProxyRule = defineRule({
532
+ meta: {
533
+ type: "problem",
534
+ docs: {
535
+ description: "Enforce createContextProxy pattern for getter exports",
536
+ recommended: true
537
+ },
538
+ messages: {
539
+ nonStringArg:
540
+ "Getter export '{{name}}' calls context() with a non-string argument. Use a string literal: context('fieldName').",
541
+ notContextProxy:
542
+ "Getter export '{{name}}' is not created via createContextProxy. Use: const context = createContextProxy<T>(); export const {{name}} = context('fieldName');",
543
+ missingArg:
544
+ "Getter export '{{name}}' calls context() without arguments. Provide the context field name: context('fieldName').",
545
+ missingGetPrefix:
546
+ "Export '{{name}}' uses createContextProxy but is missing the 'get' prefix. Rename to 'get{{suggested}}' so the UI plugin can detect it.",
547
+ getterNameMismatch:
548
+ "Getter '{{name}}' does not match context field '{{field}}'. Expected export name: 'get{{expected}}' (derived: '{{derived}}', actual field: '{{field}}'). The UI plugin uses deriveContextField which strips 'get' and lowercases the next char."
549
+ },
550
+ schema: []
551
+ },
552
+
553
+ create(context: Context) {
554
+ let createContextProxyLocalName: string | null = null;
555
+ const proxyVariableNames = new Set<string>();
556
+
557
+ return {
558
+ ImportDeclaration(node: Node) {
559
+ const importNode = node as unknown as Record<string, unknown>;
560
+ const source = importNode.source as Record<string, unknown> | undefined;
561
+ if (!source || typeof source.value !== "string") return;
562
+ if (source.value !== "@qds.dev/base") return;
563
+
564
+ const specifiers = importNode.specifiers as
565
+ | Array<Record<string, unknown>>
566
+ | undefined;
567
+ if (!Array.isArray(specifiers)) return;
568
+
569
+ for (const specifier of specifiers) {
570
+ if (specifier.type !== "ImportSpecifier") continue;
571
+ const imported = specifier.imported as Record<string, unknown> | undefined;
572
+ const local = specifier.local as Record<string, unknown> | undefined;
573
+ if (!imported || !local) continue;
574
+ if (typeof imported.name !== "string") continue;
575
+ if (imported.name !== "createContextProxy") continue;
576
+ if (typeof local.name === "string") {
577
+ createContextProxyLocalName = local.name;
578
+ }
579
+ }
580
+ },
581
+
582
+ VariableDeclarator(node: Node) {
583
+ if (!createContextProxyLocalName) return;
584
+
585
+ const decl = node as unknown as ESTree.VariableDeclarator;
586
+ if (!decl.init) return;
587
+ if (decl.init.type !== "CallExpression") return;
588
+
589
+ const callExpr = decl.init as unknown as ESTree.CallExpression;
590
+ if (callExpr.callee.type !== "Identifier") return;
591
+
592
+ const calleeName = (callExpr.callee as unknown as ESTree.Identifier).name;
593
+ if (calleeName !== createContextProxyLocalName) return;
594
+ if (decl.id.type !== "Identifier") return;
595
+
596
+ const varName = (decl.id as unknown as ESTree.Identifier).name;
597
+ proxyVariableNames.add(varName);
598
+ },
599
+
600
+ ExportNamedDeclaration(node: Node) {
601
+ if (!createContextProxyLocalName) return;
602
+
603
+ const exportNode = node as unknown as Record<string, unknown>;
604
+ const declaration = exportNode.declaration as Record<string, unknown> | undefined;
605
+ if (!declaration) return;
606
+ if (declaration.type !== "VariableDeclaration") return;
607
+
608
+ const declarations = declaration.declarations as
609
+ | Array<Record<string, unknown>>
610
+ | undefined;
611
+ if (!Array.isArray(declarations)) return;
612
+
613
+ for (const declarator of declarations) {
614
+ const id = declarator.id as Record<string, unknown> | undefined;
615
+ if (!id || id.type !== "Identifier") continue;
616
+
617
+ const exportName = id.name as string;
618
+ const init = declarator.init as Record<string, unknown> | undefined;
619
+ if (!init) continue;
620
+ if (init.type !== "CallExpression") continue;
621
+
622
+ const callee = init.callee as Record<string, unknown> | undefined;
623
+ if (!callee || callee.type !== "Identifier") continue;
624
+
625
+ const calleeName = callee.name as string;
626
+ const hasGetPrefix = exportName.startsWith("get") && exportName.length > 3;
627
+
628
+ if (!hasGetPrefix && proxyVariableNames.has(calleeName)) {
629
+ const suggested = exportName.charAt(0).toUpperCase() + exportName.slice(1);
630
+ context.report({
631
+ node: node as unknown as Ranged,
632
+ messageId: "missingGetPrefix",
633
+ data: { name: exportName, suggested }
634
+ });
635
+ continue;
636
+ }
637
+
638
+ if (!hasGetPrefix) continue;
639
+
640
+ if (!proxyVariableNames.has(calleeName)) {
641
+ context.report({
642
+ node: node as unknown as Ranged,
643
+ messageId: "notContextProxy",
644
+ data: { name: exportName }
645
+ });
646
+ continue;
647
+ }
648
+
649
+ const args = init.arguments as Array<Record<string, unknown>> | undefined;
650
+ if (!Array.isArray(args) || args.length === 0) {
651
+ context.report({
652
+ node: node as unknown as Ranged,
653
+ messageId: "missingArg",
654
+ data: { name: exportName }
655
+ });
656
+ continue;
657
+ }
658
+
659
+ const firstArg = args[0];
660
+ const isStringLiteral =
661
+ firstArg.type === "Literal" && typeof firstArg.value === "string";
662
+ if (!isStringLiteral) {
663
+ context.report({
664
+ node: node as unknown as Ranged,
665
+ messageId: "nonStringArg",
666
+ data: { name: exportName }
667
+ });
668
+ continue;
669
+ }
670
+
671
+ const contextField = firstArg.value as string;
672
+ const derived = exportName.slice(3, 4).toLowerCase() + exportName.slice(4);
673
+ if (derived !== contextField) {
674
+ context.report({
675
+ node: node as unknown as Ranged,
676
+ messageId: "getterNameMismatch",
677
+ data: {
678
+ name: exportName,
679
+ field: contextField,
680
+ expected: contextField.charAt(0).toUpperCase() + contextField.slice(1),
681
+ derived
682
+ }
683
+ });
684
+ }
685
+ }
686
+ }
687
+ };
688
+ }
689
+ });
690
+
691
+ const qdsPlugin = definePlugin({
692
+ meta: {
693
+ name: "qds-internal"
694
+ },
695
+ rules: {
696
+ "no-default-name": noDefaultXNamingRule,
697
+ "event-handlers-array-pattern": eventHandlersArrayPatternRule,
698
+ "one-element-composition": oneElementCompositionRule,
699
+ "require-use-bindings": requireUseBindingsRule,
700
+ "require-destructure-bindings": requireDestructureBindingsRule,
701
+ "require-research-file": requireResearchFileRule,
702
+ "require-test-file": requireTestFileRule,
703
+ "require-context-proxy": requireContextProxyRule
704
+ }
705
+ });
706
+
707
+ export default qdsPlugin;