@memberjunction/react-test-harness 2.90.0 → 2.91.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.
@@ -30,11 +30,111 @@ exports.ComponentLinter = void 0;
30
30
  const parser = __importStar(require("@babel/parser"));
31
31
  const traverse_1 = __importDefault(require("@babel/traverse"));
32
32
  const t = __importStar(require("@babel/types"));
33
+ // Standard HTML elements (lowercase)
34
+ const HTML_ELEMENTS = new Set([
35
+ // Main root
36
+ 'html',
37
+ // Document metadata
38
+ 'base', 'head', 'link', 'meta', 'style', 'title',
39
+ // Sectioning root
40
+ 'body',
41
+ // Content sectioning
42
+ 'address', 'article', 'aside', 'footer', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
43
+ 'main', 'nav', 'section',
44
+ // Text content
45
+ 'blockquote', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure', 'hr', 'li', 'menu', 'ol', 'p', 'pre', 'ul',
46
+ // Inline text semantics
47
+ 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd', 'mark',
48
+ 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',
49
+ // Image and multimedia
50
+ 'area', 'audio', 'img', 'map', 'track', 'video',
51
+ // Embedded content
52
+ 'embed', 'iframe', 'object', 'param', 'picture', 'portal', 'source',
53
+ // SVG and MathML
54
+ 'svg', 'math',
55
+ // Scripting
56
+ 'canvas', 'noscript', 'script',
57
+ // Demarcating edits
58
+ 'del', 'ins',
59
+ // Table content
60
+ 'caption', 'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr',
61
+ // Forms
62
+ 'button', 'datalist', 'fieldset', 'form', 'input', 'label', 'legend', 'meter', 'optgroup',
63
+ 'option', 'output', 'progress', 'select', 'textarea',
64
+ // Interactive elements
65
+ 'details', 'dialog', 'summary',
66
+ // Web Components
67
+ 'slot', 'template',
68
+ // SVG elements (common ones)
69
+ 'animate', 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'defs', 'desc', 'ellipse',
70
+ 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix',
71
+ 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood',
72
+ 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode',
73
+ 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile',
74
+ 'feTurbulence', 'filter', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker',
75
+ 'mask', 'metadata', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect',
76
+ 'stop', 'switch', 'symbol', 'text', 'textPath', 'tspan', 'use', 'view'
77
+ ]);
78
+ // React built-in components (PascalCase)
79
+ const REACT_BUILT_INS = new Set([
80
+ 'Fragment',
81
+ 'StrictMode',
82
+ 'Suspense',
83
+ 'Profiler'
84
+ ]);
33
85
  // Helper function
34
86
  function getLineNumber(code, index) {
35
87
  return code.substring(0, index).split('\n').length;
36
88
  }
89
+ // Extract property names from TypeScript types at compile time
90
+ // These will be evaluated at TypeScript compile time and become static arrays
91
+ const runQueryResultProps = [
92
+ 'QueryID', 'QueryName', 'Success', 'Results', 'RowCount',
93
+ 'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
94
+ ];
95
+ const runViewResultProps = [
96
+ 'Success', 'Results', 'UserViewRunID', 'RowCount',
97
+ 'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
98
+ ];
37
99
  class ComponentLinter {
100
+ // Helper method to check if a variable comes from RunQuery or RunView
101
+ static isVariableFromRunQueryOrView(path, varName, methodName) {
102
+ let isFromMethod = false;
103
+ // Look up the binding for this variable
104
+ const binding = path.scope.getBinding(varName);
105
+ if (binding && binding.path) {
106
+ // Check if it's from a .then() or await of RunQuery/RunView
107
+ const parent = binding.path.parent;
108
+ if (t.isVariableDeclarator(binding.path.node)) {
109
+ const init = binding.path.node.init;
110
+ // Check for await utilities.rq.RunQuery or utilities.rv.RunView
111
+ if (t.isAwaitExpression(init) && t.isCallExpression(init.argument)) {
112
+ const callee = init.argument.callee;
113
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
114
+ if (callee.property.name === methodName ||
115
+ callee.property.name === methodName + 's') { // RunViews
116
+ isFromMethod = true;
117
+ }
118
+ }
119
+ }
120
+ // Check for .then() pattern
121
+ if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
122
+ if (t.isIdentifier(init.callee.property) && init.callee.property.name === 'then') {
123
+ // Check if the object being called is RunQuery/RunView
124
+ const obj = init.callee.object;
125
+ if (t.isCallExpression(obj) && t.isMemberExpression(obj.callee)) {
126
+ if (t.isIdentifier(obj.callee.property) &&
127
+ (obj.callee.property.name === methodName ||
128
+ obj.callee.property.name === methodName + 's')) {
129
+ isFromMethod = true;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return isFromMethod;
137
+ }
38
138
  static async lintComponent(code, componentName, componentSpec, isRootComponent) {
39
139
  try {
40
140
  const ast = parser.parse(code, {
@@ -103,6 +203,8 @@ class ComponentLinter {
103
203
  // Extract entity names from dataRequirements
104
204
  const requiredEntities = new Set();
105
205
  const requiredQueries = new Set();
206
+ // Map to store full query definitions for parameter validation
207
+ const queryDefinitionsMap = new Map();
106
208
  // Map to track allowed fields per entity
107
209
  const entityFieldsMap = new Map();
108
210
  if (componentSpec.dataRequirements?.entities) {
@@ -121,6 +223,7 @@ class ComponentLinter {
121
223
  for (const query of componentSpec.dataRequirements.queries) {
122
224
  if (query.name) {
123
225
  requiredQueries.add(query.name);
226
+ queryDefinitionsMap.set(query.name, query);
124
227
  }
125
228
  }
126
229
  }
@@ -152,6 +255,7 @@ class ComponentLinter {
152
255
  for (const query of dep.dataRequirements.queries) {
153
256
  if (query.name) {
154
257
  requiredQueries.add(query.name);
258
+ queryDefinitionsMap.set(query.name, query);
155
259
  }
156
260
  }
157
261
  }
@@ -189,17 +293,38 @@ class ComponentLinter {
189
293
  const usedEntity = prop.value.value;
190
294
  // Check if this entity is in the required entities
191
295
  if (requiredEntities.size > 0 && !requiredEntities.has(usedEntity)) {
192
- // Try to find the closest match
193
- const possibleMatches = Array.from(requiredEntities).filter(e => e.toLowerCase().includes(usedEntity.toLowerCase()) ||
194
- usedEntity.toLowerCase().includes(e.toLowerCase()));
296
+ // Enhanced fuzzy matching for better suggestions
297
+ const possibleMatches = Array.from(requiredEntities).filter(e => {
298
+ const eLower = e.toLowerCase();
299
+ const usedLower = usedEntity.toLowerCase();
300
+ // Check various matching patterns
301
+ return (
302
+ // Contains match
303
+ eLower.includes(usedLower) ||
304
+ usedLower.includes(eLower) ||
305
+ // Remove spaces and check
306
+ eLower.replace(/\s+/g, '').includes(usedLower.replace(/\s+/g, '')) ||
307
+ usedLower.replace(/\s+/g, '').includes(eLower.replace(/\s+/g, '')) ||
308
+ // Check if the main words match (ignore prefixes like "MJ:")
309
+ eLower.replace(/^mj:\s*/i, '').includes(usedLower) ||
310
+ usedLower.includes(eLower.replace(/^mj:\s*/i, '')));
311
+ });
312
+ // Always show all available entities for clarity
313
+ const allEntities = Array.from(requiredEntities);
314
+ const entityList = allEntities.length <= 5
315
+ ? allEntities.join(', ')
316
+ : allEntities.slice(0, 5).join(', ') + `, ... (${allEntities.length} total)`;
317
+ let message = `Entity "${usedEntity}" not found in dataRequirements.`;
318
+ if (possibleMatches.length > 0) {
319
+ message += ` Did you mean "${possibleMatches[0]}"?`;
320
+ }
321
+ message += ` Available entities: ${entityList}`;
195
322
  violations.push({
196
323
  rule: 'entity-name-mismatch',
197
324
  severity: 'critical',
198
325
  line: prop.value.loc?.start.line || 0,
199
326
  column: prop.value.loc?.start.column || 0,
200
- message: `Entity "${usedEntity}" not found in dataRequirements. ${possibleMatches.length > 0
201
- ? `Did you mean "${possibleMatches[0]}"?`
202
- : `Available entities: ${Array.from(requiredEntities).join(', ')}`}`,
327
+ message,
203
328
  code: `EntityName: "${usedEntity}"`
204
329
  });
205
330
  }
@@ -288,22 +413,104 @@ class ComponentLinter {
288
413
  const usedQuery = prop.value.value;
289
414
  // Check if this query is in the required queries
290
415
  if (requiredQueries.size > 0 && !requiredQueries.has(usedQuery)) {
291
- // Try to find the closest match
292
- const possibleMatches = Array.from(requiredQueries).filter(q => q.toLowerCase().includes(usedQuery.toLowerCase()) ||
293
- usedQuery.toLowerCase().includes(q.toLowerCase()));
416
+ // Enhanced fuzzy matching for better suggestions
417
+ const possibleMatches = Array.from(requiredQueries).filter(q => {
418
+ const qLower = q.toLowerCase();
419
+ const usedLower = usedQuery.toLowerCase();
420
+ return (
421
+ // Contains match
422
+ qLower.includes(usedLower) ||
423
+ usedLower.includes(qLower) ||
424
+ // Remove spaces and check
425
+ qLower.replace(/\s+/g, '').includes(usedLower.replace(/\s+/g, '')) ||
426
+ usedLower.replace(/\s+/g, '').includes(qLower.replace(/\s+/g, '')));
427
+ });
428
+ // Always show all available queries for clarity
429
+ const allQueries = Array.from(requiredQueries);
430
+ const queryList = allQueries.length <= 5
431
+ ? allQueries.join(', ')
432
+ : allQueries.slice(0, 5).join(', ') + `, ... (${allQueries.length} total)`;
433
+ let message = `Query "${usedQuery}" not found in dataRequirements.`;
434
+ if (possibleMatches.length > 0) {
435
+ message += ` Did you mean "${possibleMatches[0]}"?`;
436
+ }
437
+ if (requiredQueries.size > 0) {
438
+ message += ` Available queries: ${queryList}`;
439
+ }
440
+ else {
441
+ message += ` No queries defined in dataRequirements.`;
442
+ }
294
443
  violations.push({
295
444
  rule: 'query-name-mismatch',
296
445
  severity: 'critical',
297
446
  line: prop.value.loc?.start.line || 0,
298
447
  column: prop.value.loc?.start.column || 0,
299
- message: `Query "${usedQuery}" not found in dataRequirements. ${possibleMatches.length > 0
300
- ? `Did you mean "${possibleMatches[0]}"?`
301
- : requiredQueries.size > 0
302
- ? `Available queries: ${Array.from(requiredQueries).join(', ')}`
303
- : `No queries defined in dataRequirements`}`,
448
+ message,
304
449
  code: `QueryName: "${usedQuery}"`
305
450
  });
306
451
  }
452
+ else if (queryDefinitionsMap.has(usedQuery)) {
453
+ // Query is valid, now check parameters
454
+ const queryDef = queryDefinitionsMap.get(usedQuery);
455
+ if (queryDef?.parameters && queryDef.parameters.length > 0) {
456
+ // Extract parameters from the RunQuery call
457
+ const paramsInCall = new Map();
458
+ // Look for Parameters property in the config object
459
+ for (const prop of configObj.properties) {
460
+ if (t.isObjectProperty(prop) &&
461
+ t.isIdentifier(prop.key) &&
462
+ prop.key.name === 'Parameters' &&
463
+ t.isObjectExpression(prop.value)) {
464
+ // Extract each parameter from the Parameters object
465
+ for (const paramProp of prop.value.properties) {
466
+ if (t.isObjectProperty(paramProp) && t.isIdentifier(paramProp.key)) {
467
+ paramsInCall.set(paramProp.key.name, paramProp);
468
+ }
469
+ }
470
+ // Check for required parameters
471
+ const requiredParams = queryDef.parameters.filter(p => p.value !== '@runtime' || p.value === '@runtime');
472
+ for (const reqParam of requiredParams) {
473
+ if (!paramsInCall.has(reqParam.name)) {
474
+ violations.push({
475
+ rule: 'missing-query-parameter',
476
+ severity: 'critical',
477
+ line: prop.value.loc?.start.line || 0,
478
+ column: prop.value.loc?.start.column || 0,
479
+ message: `Missing required parameter "${reqParam.name}" for query "${usedQuery}". ${reqParam.description ? `Description: ${reqParam.description}` : ''}`,
480
+ code: `Parameters: { ${reqParam.name}: ... }`
481
+ });
482
+ }
483
+ }
484
+ // Check for unknown parameters
485
+ const validParamNames = new Set(queryDef.parameters.map(p => p.name));
486
+ for (const [paramName, paramNode] of paramsInCall) {
487
+ if (!validParamNames.has(paramName)) {
488
+ violations.push({
489
+ rule: 'unknown-query-parameter',
490
+ severity: 'high',
491
+ line: paramNode.loc?.start.line || 0,
492
+ column: paramNode.loc?.start.column || 0,
493
+ message: `Unknown parameter "${paramName}" for query "${usedQuery}". Valid parameters: ${Array.from(validParamNames).join(', ')}`,
494
+ code: `${paramName}: ...`
495
+ });
496
+ }
497
+ }
498
+ break; // Found Parameters property, no need to continue
499
+ }
500
+ }
501
+ // If query has parameters but no Parameters property was found in the call
502
+ if (paramsInCall.size === 0 && queryDef.parameters.length > 0) {
503
+ violations.push({
504
+ rule: 'missing-parameters-object',
505
+ severity: 'critical',
506
+ line: configObj.loc?.start.line || 0,
507
+ column: configObj.loc?.start.column || 0,
508
+ message: `Query "${usedQuery}" requires parameters but none were provided. Required parameters: ${queryDef.parameters.map(p => p.name).join(', ')}`,
509
+ code: `RunQuery({ QueryName: "${usedQuery}", Parameters: { ... } })`
510
+ });
511
+ }
512
+ }
513
+ }
307
514
  }
308
515
  }
309
516
  }
@@ -359,6 +566,211 @@ class ComponentLinter {
359
566
  const suggestions = [];
360
567
  for (const violation of violations) {
361
568
  switch (violation.rule) {
569
+ case 'no-import-statements':
570
+ suggestions.push({
571
+ violation: violation.rule,
572
+ suggestion: 'Remove all import statements. Interactive components receive everything through props.',
573
+ example: `// ❌ WRONG - Using import statements:
574
+ import React from 'react';
575
+ import { useState } from 'react';
576
+ import { format } from 'date-fns';
577
+ import './styles.css';
578
+
579
+ function MyComponent({ utilities, styles }) {
580
+ // ...
581
+ }
582
+
583
+ // ✅ CORRECT - Everything passed as props:
584
+ function MyComponent({ utilities, styles, components }) {
585
+ // React hooks are available globally (useState, useEffect, etc.)
586
+ const [value, setValue] = useState('');
587
+
588
+ // Utilities include formatting functions
589
+ const formatted = utilities.formatDate(new Date());
590
+
591
+ // Styles are passed as props
592
+ return <div style={styles.container}>...</div>;
593
+ }
594
+
595
+ // All dependencies must be:
596
+ // 1. Passed through the 'utilities' prop (formatting, helpers)
597
+ // 2. Passed through the 'components' prop (child components)
598
+ // 3. Passed through the 'styles' prop (styling)
599
+ // 4. Available globally (React hooks)`
600
+ });
601
+ break;
602
+ case 'no-export-statements':
603
+ suggestions.push({
604
+ violation: violation.rule,
605
+ suggestion: 'Remove all export statements. The component function should be the only code, not exported.',
606
+ example: `// ❌ WRONG - Using export:
607
+ export function MyComponent({ utilities }) {
608
+ return <div>Hello</div>;
609
+ }
610
+
611
+ export const helper = () => {};
612
+ export default MyComponent;
613
+
614
+ // ✅ CORRECT - Just the function, no exports:
615
+ function MyComponent({ utilities, styles, components }) {
616
+ // Helper functions defined inside if needed
617
+ const helper = () => {
618
+ // ...
619
+ };
620
+
621
+ return <div>Hello</div>;
622
+ }
623
+
624
+ // The component is self-contained.
625
+ // No exports needed - the host environment
626
+ // will execute the function directly.`
627
+ });
628
+ break;
629
+ case 'no-require-statements':
630
+ suggestions.push({
631
+ violation: violation.rule,
632
+ suggestion: 'Remove all require() and dynamic import() statements. Use props instead.',
633
+ example: `// ❌ WRONG - Using require or dynamic import:
634
+ function MyComponent({ utilities }) {
635
+ const lodash = require('lodash');
636
+ const module = await import('./module');
637
+
638
+ return <div>...</div>;
639
+ }
640
+
641
+ // ✅ CORRECT - Use utilities and components props:
642
+ function MyComponent({ utilities, styles, components }) {
643
+ // Use utilities for helper functions
644
+ const result = utilities.debounce(() => {
645
+ // ...
646
+ }, 300);
647
+
648
+ // Use components prop for child components
649
+ const { DataTable, FilterPanel } = components;
650
+
651
+ return (
652
+ <div>
653
+ <DataTable {...props} />
654
+ <FilterPanel {...props} />
655
+ </div>
656
+ );
657
+ }
658
+
659
+ // Everything the component needs must be:
660
+ // - Passed via props (utilities, components, styles)
661
+ // - Available globally (React hooks)
662
+ // No module loading allowed!`
663
+ });
664
+ break;
665
+ case 'use-function-declaration':
666
+ suggestions.push({
667
+ violation: violation.rule,
668
+ suggestion: 'Use function declaration syntax for TOP-LEVEL component definitions. Arrow functions are fine inside components.',
669
+ example: `// ❌ WRONG - Top-level arrow function component:
670
+ const MyComponent = ({ utilities, styles, components }) => {
671
+ const [state, setState] = useState('');
672
+
673
+ return <div>{state}</div>;
674
+ };
675
+
676
+ // ✅ CORRECT - Function declaration for top-level:
677
+ function MyComponent({ utilities, styles, components }) {
678
+ const [state, setState] = useState('');
679
+
680
+ // Arrow functions are FINE inside the component:
681
+ const handleClick = () => {
682
+ setState('clicked');
683
+ };
684
+
685
+ const ChildComponent = () => <div>This is OK inside the component</div>;
686
+
687
+ return <div onClick={handleClick}>{state}</div>;
688
+ }
689
+
690
+ // Child components also use function declaration:
691
+ function ChildComponent() {
692
+ return <div>Child</div>;
693
+ }
694
+
695
+ // Why function declarations?
696
+ // 1. Clearer component identification
697
+ // 2. Better debugging experience (named functions)
698
+ // 3. Hoisting allows flexible code organization
699
+ // 4. Consistent with React documentation patterns
700
+ // 5. Easier to distinguish from regular variables`
701
+ });
702
+ break;
703
+ case 'no-return-component':
704
+ suggestions.push({
705
+ violation: violation.rule,
706
+ suggestion: 'Remove the return statement at the end of the file. The component function should stand alone.',
707
+ example: `// ❌ WRONG - Returning the component:
708
+ function MyComponent({ utilities, styles, components }) {
709
+ const [state, setState] = useState('');
710
+
711
+ return <div>{state}</div>;
712
+ }
713
+
714
+ return MyComponent; // <-- Remove this!
715
+
716
+ // ❌ ALSO WRONG - Component reference at end:
717
+ function MyComponent({ utilities, styles, components }) {
718
+ return <div>Hello</div>;
719
+ }
720
+
721
+ MyComponent; // <-- Remove this!
722
+
723
+ // ✅ CORRECT - Just the function declaration:
724
+ function MyComponent({ utilities, styles, components }) {
725
+ const [state, setState] = useState('');
726
+
727
+ return <div>{state}</div>;
728
+ }
729
+ // Nothing after the function - file ends here
730
+
731
+ // The runtime will find and execute your component
732
+ // by its function name. No need to return or reference it!`
733
+ });
734
+ break;
735
+ case 'no-iife-wrapper':
736
+ suggestions.push({
737
+ violation: violation.rule,
738
+ suggestion: 'Remove the IIFE wrapper. Component code should be plain functions, not wrapped in immediately invoked functions.',
739
+ example: `// ❌ WRONG - IIFE wrapper patterns:
740
+ (function() {
741
+ function MyComponent({ utilities, styles, components }) {
742
+ return <div>Hello</div>;
743
+ }
744
+ return MyComponent;
745
+ })();
746
+
747
+ // Also wrong:
748
+ (function() {
749
+ const MyComponent = ({ utilities }) => {
750
+ return <div>Hello</div>;
751
+ };
752
+ })();
753
+
754
+ // Also wrong - arrow function IIFE:
755
+ (() => {
756
+ function MyComponent({ utilities }) {
757
+ return <div>Hello</div>;
758
+ }
759
+ })();
760
+
761
+ // ✅ CORRECT - Direct function declaration:
762
+ function MyComponent({ utilities, styles, components }) {
763
+ return <div>Hello</div>;
764
+ }
765
+
766
+ // Why no IIFE?
767
+ // 1. Components run in their own scope already
768
+ // 2. The runtime handles isolation
769
+ // 3. IIFEs prevent proper component discovery
770
+ // 4. Makes debugging harder
771
+ // 5. Unnecessary complexity`
772
+ });
773
+ break;
362
774
  case 'full-state-ownership':
363
775
  suggestions.push({
364
776
  violation: violation.rule,
@@ -634,6 +1046,86 @@ await utilities.rv.RunViews([
634
1046
  // match those declared in the component spec's dataRequirements`
635
1047
  });
636
1048
  break;
1049
+ case 'missing-query-parameter':
1050
+ suggestions.push({
1051
+ violation: violation.rule,
1052
+ suggestion: 'Provide all required parameters defined in dataRequirements for the query',
1053
+ example: `// The component spec defines required parameters:
1054
+ // dataRequirements: {
1055
+ // queries: [
1056
+ // {
1057
+ // name: "User Activity Summary",
1058
+ // parameters: [
1059
+ // { name: "UserID", value: "@runtime", description: "User to filter by" },
1060
+ // { name: "StartDate", value: "@runtime", description: "Start of date range" }
1061
+ // ]
1062
+ // }
1063
+ // ]
1064
+ // }
1065
+
1066
+ // ❌ WRONG - Missing required parameter:
1067
+ await utilities.rq.RunQuery({
1068
+ QueryName: "User Activity Summary",
1069
+ Parameters: {
1070
+ UserID: currentUserId
1071
+ // Missing StartDate!
1072
+ }
1073
+ });
1074
+
1075
+ // ✅ CORRECT - All required parameters provided:
1076
+ await utilities.rq.RunQuery({
1077
+ QueryName: "User Activity Summary",
1078
+ Parameters: {
1079
+ UserID: currentUserId,
1080
+ StartDate: startDate // All parameters included
1081
+ }
1082
+ });`
1083
+ });
1084
+ break;
1085
+ case 'unknown-query-parameter':
1086
+ suggestions.push({
1087
+ violation: violation.rule,
1088
+ suggestion: 'Only use parameters that are defined in dataRequirements for the query',
1089
+ example: `// ❌ WRONG - Using undefined parameter:
1090
+ await utilities.rq.RunQuery({
1091
+ QueryName: "User Activity Summary",
1092
+ Parameters: {
1093
+ UserID: currentUserId,
1094
+ EndDate: endDate, // Not defined in dataRequirements!
1095
+ ExtraParam: 123 // Unknown parameter!
1096
+ }
1097
+ });
1098
+
1099
+ // ✅ CORRECT - Only use defined parameters:
1100
+ await utilities.rq.RunQuery({
1101
+ QueryName: "User Activity Summary",
1102
+ Parameters: {
1103
+ UserID: currentUserId,
1104
+ StartDate: startDate // Only parameters from dataRequirements
1105
+ }
1106
+ });`
1107
+ });
1108
+ break;
1109
+ case 'missing-parameters-object':
1110
+ suggestions.push({
1111
+ violation: violation.rule,
1112
+ suggestion: 'Queries with parameters must include a Parameters object in RunQuery',
1113
+ example: `// ❌ WRONG - Query requires parameters but none provided:
1114
+ await utilities.rq.RunQuery({
1115
+ QueryName: "User Activity Summary"
1116
+ // Missing Parameters object!
1117
+ });
1118
+
1119
+ // ✅ CORRECT - Include Parameters object:
1120
+ await utilities.rq.RunQuery({
1121
+ QueryName: "User Activity Summary",
1122
+ Parameters: {
1123
+ UserID: currentUserId,
1124
+ StartDate: startDate
1125
+ }
1126
+ });`
1127
+ });
1128
+ break;
637
1129
  case 'query-name-mismatch':
638
1130
  suggestions.push({
639
1131
  violation: violation.rule,
@@ -995,6 +1487,57 @@ function RootComponent({ utilities, styles, components, callbacks, savedUserSett
995
1487
  }`
996
1488
  });
997
1489
  break;
1490
+ case 'runview-runquery-result-direct-usage':
1491
+ suggestions.push({
1492
+ violation: violation.rule,
1493
+ suggestion: 'RunView and RunQuery return result objects, not arrays. Access the data with .Results property.',
1494
+ example: `// ❌ WRONG - Using result directly as array:
1495
+ const result = await utilities.rv.RunView({
1496
+ EntityName: 'Users',
1497
+ Fields: ['ID', 'Name']
1498
+ });
1499
+
1500
+ // These will all fail:
1501
+ setUsers(result); // Wrong! result is an object
1502
+ result.map(u => u.Name); // Wrong! Can't map on object
1503
+ const users = Array.isArray(result) ? result : []; // Wrong! Will always be []
1504
+
1505
+ // ✅ CORRECT - Access the Results property:
1506
+ const result = await utilities.rv.RunView({
1507
+ EntityName: 'Users',
1508
+ Fields: ['ID', 'Name']
1509
+ });
1510
+
1511
+ // Check success first (recommended):
1512
+ if (result.Success) {
1513
+ setUsers(result.Results || []);
1514
+ } else {
1515
+ console.error('Failed:', result.ErrorMessage);
1516
+ setUsers([]);
1517
+ }
1518
+
1519
+ // Or use optional chaining:
1520
+ setUsers(result?.Results || []);
1521
+
1522
+ // Now array methods work:
1523
+ const names = result.Results?.map(u => u.Name) || [];
1524
+
1525
+ // ✅ For RunQuery - same pattern:
1526
+ const queryResult = await utilities.rq.RunQuery({
1527
+ QueryName: 'UserSummary'
1528
+ });
1529
+ setData(queryResult.Results || []); // NOT queryResult directly!
1530
+
1531
+ // Result object structure:
1532
+ // {
1533
+ // Success: boolean,
1534
+ // Results: Array, // Your data is here!
1535
+ // ErrorMessage?: string,
1536
+ // TotalRowCount?: number,
1537
+ // ExecutionTime?: number
1538
+ // }`
1539
+ });
1540
+ break;
998
1541
  }
999
1542
  }
1000
1543
  return suggestions;
@@ -1004,118 +1547,94 @@ exports.ComponentLinter = ComponentLinter;
1004
1547
  // Universal rules that apply to all components with SavedUserSettings pattern
1005
1548
  ComponentLinter.universalComponentRules = [
1006
1549
  {
1007
- name: 'no-use-reducer',
1550
+ name: 'no-import-statements',
1008
1551
  appliesTo: 'all',
1009
1552
  test: (ast, componentName, componentSpec) => {
1010
1553
  const violations = [];
1011
1554
  (0, traverse_1.default)(ast, {
1012
- CallExpression(path) {
1013
- const callee = path.node.callee;
1014
- if ((t.isIdentifier(callee) && callee.name === 'useReducer') ||
1015
- (t.isMemberExpression(callee) &&
1016
- t.isIdentifier(callee.object) && callee.object.name === 'React' &&
1017
- t.isIdentifier(callee.property) && callee.property.name === 'useReducer')) {
1018
- violations.push({
1019
- rule: 'no-use-reducer',
1020
- severity: 'high', // High but not critical - it's a pattern violation
1021
- line: path.node.loc?.start.line || 0,
1022
- column: path.node.loc?.start.column || 0,
1023
- message: `Component "${componentName}" uses useReducer at line ${path.node.loc?.start.line}. Components should manage state with useState and persist important settings with onSaveUserSettings.`,
1024
- code: path.toString()
1025
- });
1026
- }
1555
+ ImportDeclaration(path) {
1556
+ violations.push({
1557
+ rule: 'no-import-statements',
1558
+ severity: 'critical',
1559
+ line: path.node.loc?.start.line || 0,
1560
+ column: path.node.loc?.start.column || 0,
1561
+ message: `Component "${componentName}" contains an import statement. Interactive components cannot use import statements - all dependencies must be passed as props.`,
1562
+ code: path.toString().substring(0, 100)
1563
+ });
1027
1564
  }
1028
1565
  });
1029
1566
  return violations;
1030
1567
  }
1031
1568
  },
1032
- // New rules for the controlled component pattern
1033
1569
  {
1034
- name: 'no-data-prop',
1570
+ name: 'no-export-statements',
1035
1571
  appliesTo: 'all',
1036
1572
  test: (ast, componentName, componentSpec) => {
1037
1573
  const violations = [];
1038
1574
  (0, traverse_1.default)(ast, {
1039
- // Check function parameters for 'data' prop
1040
- FunctionDeclaration(path) {
1041
- if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
1042
- const param = path.node.params[0];
1043
- if (t.isObjectPattern(param)) {
1044
- for (const prop of param.properties) {
1045
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1046
- violations.push({
1047
- rule: 'no-data-prop',
1048
- severity: 'medium', // It's a pattern issue, not critical
1049
- line: prop.loc?.start.line || 0,
1050
- column: prop.loc?.start.column || 0,
1051
- message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
1052
- code: 'data prop in component signature'
1053
- });
1054
- }
1055
- }
1056
- }
1057
- }
1575
+ ExportNamedDeclaration(path) {
1576
+ violations.push({
1577
+ rule: 'no-export-statements',
1578
+ severity: 'critical',
1579
+ line: path.node.loc?.start.line || 0,
1580
+ column: path.node.loc?.start.column || 0,
1581
+ message: `Component "${componentName}" contains an export statement. Interactive components are self-contained and cannot export values.`,
1582
+ code: path.toString().substring(0, 100)
1583
+ });
1058
1584
  },
1059
- // Also check arrow functions
1060
- VariableDeclarator(path) {
1061
- if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
1062
- const init = path.node.init;
1063
- if (t.isArrowFunctionExpression(init) && init.params[0]) {
1064
- const param = init.params[0];
1065
- if (t.isObjectPattern(param)) {
1066
- for (const prop of param.properties) {
1067
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1068
- violations.push({
1069
- rule: 'no-data-prop',
1070
- severity: 'critical',
1071
- line: prop.loc?.start.line || 0,
1072
- column: prop.loc?.start.column || 0,
1073
- message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
1074
- code: 'data prop in component signature'
1075
- });
1076
- }
1077
- }
1078
- }
1079
- }
1080
- }
1585
+ ExportDefaultDeclaration(path) {
1586
+ violations.push({
1587
+ rule: 'no-export-statements',
1588
+ severity: 'critical',
1589
+ line: path.node.loc?.start.line || 0,
1590
+ column: path.node.loc?.start.column || 0,
1591
+ message: `Component "${componentName}" contains an export default statement. Interactive components are self-contained and cannot export values.`,
1592
+ code: path.toString().substring(0, 100)
1593
+ });
1594
+ },
1595
+ ExportAllDeclaration(path) {
1596
+ violations.push({
1597
+ rule: 'no-export-statements',
1598
+ severity: 'critical',
1599
+ line: path.node.loc?.start.line || 0,
1600
+ column: path.node.loc?.start.column || 0,
1601
+ message: `Component "${componentName}" contains an export * statement. Interactive components are self-contained and cannot export values.`,
1602
+ code: path.toString().substring(0, 100)
1603
+ });
1081
1604
  }
1082
1605
  });
1083
1606
  return violations;
1084
1607
  }
1085
1608
  },
1086
1609
  {
1087
- name: 'saved-user-settings-pattern',
1610
+ name: 'no-require-statements',
1088
1611
  appliesTo: 'all',
1089
1612
  test: (ast, componentName, componentSpec) => {
1090
1613
  const violations = [];
1091
- // Check for improper onSaveUserSettings usage
1092
1614
  (0, traverse_1.default)(ast, {
1093
1615
  CallExpression(path) {
1094
1616
  const callee = path.node.callee;
1095
- // Check for onSaveUserSettings calls
1096
- if (t.isMemberExpression(callee) &&
1097
- t.isIdentifier(callee.object) && callee.object.name === 'onSaveUserSettings') {
1098
- // Check if saving ephemeral state
1099
- if (path.node.arguments.length > 0) {
1100
- const arg = path.node.arguments[0];
1101
- if (t.isObjectExpression(arg)) {
1102
- for (const prop of arg.properties) {
1103
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1104
- const key = prop.key.name;
1105
- const ephemeralPatterns = ['hover', 'dropdown', 'modal', 'loading', 'typing', 'draft', 'expanded', 'collapsed', 'focused'];
1106
- if (ephemeralPatterns.some(pattern => key.toLowerCase().includes(pattern))) {
1107
- violations.push({
1108
- rule: 'saved-user-settings-pattern',
1109
- severity: 'medium', // Pattern issue but not breaking
1110
- line: prop.loc?.start.line || 0,
1111
- column: prop.loc?.start.column || 0,
1112
- message: `Saving ephemeral UI state "${key}" to savedUserSettings. Only save important user preferences.`
1113
- });
1114
- }
1115
- }
1116
- }
1117
- }
1118
- }
1617
+ // Check for require() calls
1618
+ if (t.isIdentifier(callee) && callee.name === 'require') {
1619
+ violations.push({
1620
+ rule: 'no-require-statements',
1621
+ severity: 'critical',
1622
+ line: path.node.loc?.start.line || 0,
1623
+ column: path.node.loc?.start.column || 0,
1624
+ message: `Component "${componentName}" contains a require() statement. Interactive components cannot use require - all dependencies must be passed as props.`,
1625
+ code: path.toString().substring(0, 100)
1626
+ });
1627
+ }
1628
+ // Also check for dynamic import() calls
1629
+ if (t.isImport(callee)) {
1630
+ violations.push({
1631
+ rule: 'no-require-statements',
1632
+ severity: 'critical',
1633
+ line: path.node.loc?.start.line || 0,
1634
+ column: path.node.loc?.start.column || 0,
1635
+ message: `Component "${componentName}" contains a dynamic import() statement. Interactive components cannot use dynamic imports - all dependencies must be passed as props.`,
1636
+ code: path.toString().substring(0, 100)
1637
+ });
1119
1638
  }
1120
1639
  }
1121
1640
  });
@@ -1123,65 +1642,47 @@ ComponentLinter.universalComponentRules = [
1123
1642
  }
1124
1643
  },
1125
1644
  {
1126
- name: 'pass-standard-props',
1645
+ name: 'use-function-declaration',
1127
1646
  appliesTo: 'all',
1128
1647
  test: (ast, componentName, componentSpec) => {
1129
1648
  const violations = [];
1130
- const requiredProps = ['styles', 'utilities', 'components'];
1131
- // Build a set of our component names from componentSpec dependencies
1132
- const ourComponentNames = new Set();
1133
- // Add components from dependencies array
1134
- if (componentSpec?.dependencies) {
1135
- for (const dep of componentSpec.dependencies) {
1136
- if (dep.name) {
1137
- ourComponentNames.add(dep.name);
1138
- }
1139
- }
1140
- }
1141
- // Also find components destructured from the components prop in the code
1142
1649
  (0, traverse_1.default)(ast, {
1143
1650
  VariableDeclarator(path) {
1144
- // Look for: const { ComponentA, ComponentB } = components;
1145
- if (t.isObjectPattern(path.node.id) &&
1146
- t.isIdentifier(path.node.init) &&
1147
- path.node.init.name === 'components') {
1148
- for (const prop of path.node.id.properties) {
1149
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1150
- ourComponentNames.add(prop.key.name);
1151
- }
1152
- // Also handle renaming: { ComponentA: RenamedComponent }
1153
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
1154
- ourComponentNames.add(prop.value.name);
1155
- }
1156
- }
1651
+ // Only check TOP-LEVEL declarations (not nested inside functions)
1652
+ // This prevents flagging arrow functions inside the component
1653
+ const isTopLevel = path.getFunctionParent() === null ||
1654
+ path.scope.path.type === 'Program';
1655
+ if (!isTopLevel) {
1656
+ return; // Skip non-top-level declarations
1157
1657
  }
1158
- }
1159
- });
1160
- // Now check only our components for standard props
1161
- (0, traverse_1.default)(ast, {
1162
- JSXElement(path) {
1163
- const openingElement = path.node.openingElement;
1164
- // Only check if it's one of our components
1165
- if (t.isJSXIdentifier(openingElement.name) &&
1166
- ourComponentNames.has(openingElement.name.name)) {
1167
- const componentBeingCalled = openingElement.name.name;
1168
- const passedProps = new Set();
1169
- // Collect all props being passed
1170
- for (const attr of openingElement.attributes) {
1171
- if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
1172
- passedProps.add(attr.name.name);
1173
- }
1174
- }
1175
- // Check if required props are missing
1176
- const missingProps = requiredProps.filter(prop => !passedProps.has(prop));
1177
- if (missingProps.length > 0) {
1658
+ // Check if this is the main component being defined as arrow function
1659
+ if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
1660
+ const init = path.node.init;
1661
+ // Check if it's an arrow function
1662
+ if (t.isArrowFunctionExpression(init)) {
1178
1663
  violations.push({
1179
- rule: 'pass-standard-props',
1664
+ rule: 'use-function-declaration',
1180
1665
  severity: 'critical',
1181
- line: openingElement.loc?.start.line || 0,
1182
- column: openingElement.loc?.start.column || 0,
1183
- message: `Component "${componentBeingCalled}" is missing required props: ${missingProps.join(', ')}. All child components must receive styles, utilities, and components props.`,
1184
- code: `<${componentBeingCalled} ... />`
1666
+ line: path.node.loc?.start.line || 0,
1667
+ column: path.node.loc?.start.column || 0,
1668
+ message: `Component "${componentName}" must be defined using function declaration syntax, not arrow function.`,
1669
+ code: path.toString().substring(0, 150)
1670
+ });
1671
+ }
1672
+ }
1673
+ // Also check for any other TOP-LEVEL component-like arrow functions (starts with capital letter)
1674
+ // But ONLY at the top level, not inside the component
1675
+ if (t.isIdentifier(path.node.id) && /^[A-Z]/.test(path.node.id.name)) {
1676
+ const init = path.node.init;
1677
+ if (t.isArrowFunctionExpression(init)) {
1678
+ // Only flag if it's at the top level (parallel to main component)
1679
+ violations.push({
1680
+ rule: 'use-function-declaration',
1681
+ severity: 'high',
1682
+ line: path.node.loc?.start.line || 0,
1683
+ column: path.node.loc?.start.column || 0,
1684
+ message: `Top-level component "${path.node.id.name}" should use function declaration syntax.`,
1685
+ code: path.toString().substring(0, 150)
1185
1686
  });
1186
1687
  }
1187
1688
  }
@@ -1191,188 +1692,647 @@ ComponentLinter.universalComponentRules = [
1191
1692
  }
1192
1693
  },
1193
1694
  {
1194
- name: 'no-child-implementation',
1195
- appliesTo: 'root',
1695
+ name: 'no-return-component',
1696
+ appliesTo: 'all',
1196
1697
  test: (ast, componentName, componentSpec) => {
1197
1698
  const violations = [];
1198
- const rootFunctionName = componentName;
1199
- const declaredFunctions = [];
1200
- // First pass: collect all function declarations
1201
- (0, traverse_1.default)(ast, {
1202
- FunctionDeclaration(path) {
1203
- if (path.node.id) {
1204
- declaredFunctions.push(path.node.id.name);
1699
+ // Check for return statements at the program/top level
1700
+ if (ast.program && ast.program.body) {
1701
+ for (const statement of ast.program.body) {
1702
+ // Check for return statement returning the component
1703
+ if (t.isReturnStatement(statement)) {
1704
+ const argument = statement.argument;
1705
+ // Check if it's returning the component identifier or any identifier
1706
+ if (argument && t.isIdentifier(argument)) {
1707
+ // If it's returning the component name or any identifier at top level
1708
+ violations.push({
1709
+ rule: 'no-return-component',
1710
+ severity: 'critical',
1711
+ line: statement.loc?.start.line || 0,
1712
+ column: statement.loc?.start.column || 0,
1713
+ message: `Do not return the component at the end of the file. The component function should stand alone.`,
1714
+ code: `return ${argument.name};`
1715
+ });
1716
+ }
1717
+ }
1718
+ // Also check for expression statements that might be standalone identifiers
1719
+ if (t.isExpressionStatement(statement) &&
1720
+ t.isIdentifier(statement.expression) &&
1721
+ statement.expression.name === componentName) {
1722
+ violations.push({
1723
+ rule: 'no-return-component',
1724
+ severity: 'critical',
1725
+ line: statement.loc?.start.line || 0,
1726
+ column: statement.loc?.start.column || 0,
1727
+ message: `Do not reference the component "${componentName}" at the end of the file. The component function should stand alone.`,
1728
+ code: statement.expression.name
1729
+ });
1205
1730
  }
1206
1731
  }
1207
- });
1208
- // If there are multiple function declarations and they look like components
1209
- // (start with capital letter), it's likely implementing children
1210
- const componentFunctions = declaredFunctions.filter(name => name !== rootFunctionName && /^[A-Z]/.test(name));
1211
- if (componentFunctions.length > 0) {
1212
- violations.push({
1213
- rule: 'no-child-implementation',
1214
- severity: 'critical',
1215
- line: 1,
1216
- column: 0,
1217
- message: `Root component file contains child component implementations: ${componentFunctions.join(', ')}. Root should only reference child components, not implement them.`,
1218
- });
1219
1732
  }
1220
1733
  return violations;
1221
1734
  }
1222
1735
  },
1223
1736
  {
1224
- name: 'undefined-component-usage',
1737
+ name: 'no-window-access',
1225
1738
  appliesTo: 'all',
1226
1739
  test: (ast, componentName, componentSpec) => {
1227
1740
  const violations = [];
1228
- const componentsFromProps = new Set();
1229
- const componentsUsedInJSX = new Set();
1230
- let hasComponentsProp = false;
1741
+ // Build a map of library names to their global variables from the component spec
1742
+ const libraryMap = new Map();
1743
+ if (componentSpec?.libraries) {
1744
+ for (const lib of componentSpec.libraries) {
1745
+ if (lib.globalVariable) {
1746
+ // Store both the library name and globalVariable for lookup
1747
+ libraryMap.set(lib.name.toLowerCase(), lib.globalVariable);
1748
+ libraryMap.set(lib.globalVariable.toLowerCase(), lib.globalVariable);
1749
+ }
1750
+ }
1751
+ }
1231
1752
  (0, traverse_1.default)(ast, {
1232
- // First, find what's destructured from the components prop
1233
- VariableDeclarator(path) {
1234
- if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
1235
- // Check if destructuring from 'components'
1236
- if (path.node.init.name === 'components') {
1237
- hasComponentsProp = true;
1238
- for (const prop of path.node.id.properties) {
1239
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1240
- componentsFromProps.add(prop.key.name);
1241
- }
1753
+ MemberExpression(path) {
1754
+ // Check if accessing window object
1755
+ if (t.isIdentifier(path.node.object) && path.node.object.name === 'window') {
1756
+ // Check what property is being accessed from window
1757
+ let propertyName = '';
1758
+ let isDestructuring = false;
1759
+ if (t.isIdentifier(path.node.property)) {
1760
+ propertyName = path.node.property.name;
1761
+ }
1762
+ else if (t.isMemberExpression(path.node.property)) {
1763
+ // Handle chained access like window.Recharts.ResponsiveContainer
1764
+ const firstProp = path.node.property;
1765
+ if (t.isIdentifier(firstProp.object)) {
1766
+ propertyName = firstProp.object.name;
1242
1767
  }
1243
1768
  }
1244
- }
1245
- },
1246
- // Also check object destructuring in function parameters
1247
- FunctionDeclaration(path) {
1248
- if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
1249
- const param = path.node.params[0];
1250
- if (t.isObjectPattern(param)) {
1251
- for (const prop of param.properties) {
1252
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'components') {
1253
- hasComponentsProp = true;
1254
- // Look for nested destructuring like { components: { A, B } }
1255
- if (t.isObjectPattern(prop.value)) {
1256
- for (const innerProp of prop.value.properties) {
1257
- if (t.isObjectProperty(innerProp) && t.isIdentifier(innerProp.key)) {
1258
- componentsFromProps.add(innerProp.key.name);
1259
- }
1260
- }
1261
- }
1262
- }
1769
+ // Check if this is part of a destructuring assignment
1770
+ let currentPath = path.parentPath;
1771
+ while (currentPath) {
1772
+ if (t.isVariableDeclarator(currentPath.node) &&
1773
+ t.isObjectPattern(currentPath.node.id)) {
1774
+ isDestructuring = true;
1775
+ break;
1263
1776
  }
1777
+ currentPath = currentPath.parentPath;
1778
+ }
1779
+ // Check if the property matches a known library
1780
+ const matchedLibrary = libraryMap.get(propertyName.toLowerCase());
1781
+ if (matchedLibrary) {
1782
+ // Specific guidance for library access
1783
+ violations.push({
1784
+ rule: 'no-window-access',
1785
+ severity: 'critical',
1786
+ line: path.node.loc?.start.line || 0,
1787
+ column: path.node.loc?.start.column || 0,
1788
+ message: `Component "${componentName}" should not access window.${propertyName}. Use "${matchedLibrary}" directly - it's already available in the component's closure scope. Change "window.${propertyName}" to just "${matchedLibrary}".`,
1789
+ code: path.toString().substring(0, 100)
1790
+ });
1791
+ }
1792
+ else if (isDestructuring) {
1793
+ // Likely trying to destructure from an unknown library
1794
+ violations.push({
1795
+ rule: 'no-window-access',
1796
+ severity: 'critical',
1797
+ line: path.node.loc?.start.line || 0,
1798
+ column: path.node.loc?.start.column || 0,
1799
+ message: `Component "${componentName}" is trying to destructure from window.${propertyName}. If this is a library, it should be added to the component's libraries array in the spec and accessed via its globalVariable name.`,
1800
+ code: path.toString().substring(0, 100)
1801
+ });
1802
+ }
1803
+ else {
1804
+ // General window access
1805
+ violations.push({
1806
+ rule: 'no-window-access',
1807
+ severity: 'critical',
1808
+ line: path.node.loc?.start.line || 0,
1809
+ column: path.node.loc?.start.column || 0,
1810
+ message: `Component "${componentName}" must not access the window object. Interactive components should be self-contained and not rely on global state.`,
1811
+ code: path.toString().substring(0, 100)
1812
+ });
1264
1813
  }
1265
1814
  }
1266
1815
  },
1267
- // Track JSX element usage
1268
- JSXElement(path) {
1269
- const openingElement = path.node.openingElement;
1270
- if (t.isJSXIdentifier(openingElement.name) && /^[A-Z]/.test(openingElement.name.name)) {
1271
- const componentName = openingElement.name.name;
1272
- // Only track if it's from our destructured components
1273
- if (componentsFromProps.has(componentName)) {
1274
- componentsUsedInJSX.add(componentName);
1816
+ Identifier(path) {
1817
+ // Also check for direct window references (less common but possible)
1818
+ if (path.node.name === 'window' && path.isReferencedIdentifier()) {
1819
+ // Make sure it's not part of a member expression we already caught
1820
+ const parent = path.parent;
1821
+ if (!t.isMemberExpression(parent) || parent.object !== path.node) {
1822
+ violations.push({
1823
+ rule: 'no-window-access',
1824
+ severity: 'critical',
1825
+ line: path.node.loc?.start.line || 0,
1826
+ column: path.node.loc?.start.column || 0,
1827
+ message: `Component "${componentName}" must not reference the window object directly. Interactive components should be self-contained.`,
1828
+ code: path.toString().substring(0, 100)
1829
+ });
1275
1830
  }
1276
1831
  }
1277
1832
  }
1278
1833
  });
1279
- // Only check if we found a components prop
1280
- if (hasComponentsProp && componentsFromProps.size > 0) {
1281
- // Find components that are destructured but never used
1282
- const unusedComponents = Array.from(componentsFromProps).filter(comp => !componentsUsedInJSX.has(comp));
1283
- if (unusedComponents.length > 0) {
1284
- violations.push({
1285
- rule: 'undefined-component-usage',
1286
- severity: 'low',
1287
- line: 1,
1288
- column: 0,
1289
- message: `Component destructures ${unusedComponents.join(', ')} from components prop but never uses them. These may be missing from the component spec's dependencies array.`
1290
- });
1291
- }
1292
- }
1293
1834
  return violations;
1294
1835
  }
1295
1836
  },
1296
1837
  {
1297
- name: 'unsafe-array-access',
1838
+ name: 'no-iife-wrapper',
1298
1839
  appliesTo: 'all',
1299
1840
  test: (ast, componentName, componentSpec) => {
1300
1841
  const violations = [];
1301
- (0, traverse_1.default)(ast, {
1302
- MemberExpression(path) {
1303
- // Check for array[index] patterns
1304
- if (t.isNumericLiteral(path.node.property) ||
1305
- (t.isIdentifier(path.node.property) && path.node.computed && /^\d+$/.test(path.node.property.name))) {
1306
- // Look for patterns like: someArray[0].method()
1307
- const parent = path.parent;
1308
- if (t.isMemberExpression(parent) && parent.object === path.node) {
1309
- const code = path.toString();
1310
- // Check if it's an array access followed by a method call
1311
- if (/\[\d+\]\.\w+/.test(code)) {
1842
+ // Check if the entire code is wrapped in an IIFE
1843
+ if (ast.program && ast.program.body) {
1844
+ for (const statement of ast.program.body) {
1845
+ // Check for IIFE pattern: (function() { ... })() or (function() { ... }())
1846
+ if (t.isExpressionStatement(statement)) {
1847
+ const expr = statement.expression;
1848
+ // Pattern 1: (function() { ... })()
1849
+ if (t.isCallExpression(expr)) {
1850
+ const callee = expr.callee;
1851
+ // Check if calling a function expression wrapped in parentheses
1852
+ if (t.isParenthesizedExpression && t.isParenthesizedExpression(callee)) {
1853
+ const inner = callee.expression;
1854
+ if (t.isFunctionExpression(inner) || t.isArrowFunctionExpression(inner)) {
1855
+ violations.push({
1856
+ rule: 'no-iife-wrapper',
1857
+ severity: 'critical',
1858
+ line: statement.loc?.start.line || 0,
1859
+ column: statement.loc?.start.column || 0,
1860
+ message: `Component code must not be wrapped in an IIFE (Immediately Invoked Function Expression). Define the component function directly.`,
1861
+ code: statement.toString().substring(0, 50) + '...'
1862
+ });
1863
+ }
1864
+ }
1865
+ // Also check without ParenthesizedExpression (some parsers handle it differently)
1866
+ if (t.isFunctionExpression(callee) || t.isArrowFunctionExpression(callee)) {
1312
1867
  violations.push({
1313
- rule: 'unsafe-array-access',
1868
+ rule: 'no-iife-wrapper',
1314
1869
  severity: 'critical',
1315
- line: path.node.loc?.start.line || 0,
1316
- column: path.node.loc?.start.column || 0,
1317
- message: `Unsafe array access: ${code}. Check array bounds before accessing elements.`,
1318
- code: code
1870
+ line: statement.loc?.start.line || 0,
1871
+ column: statement.loc?.start.column || 0,
1872
+ message: `Component code must not be wrapped in an IIFE. Define the component function directly.`,
1873
+ code: statement.toString().substring(0, 50) + '...'
1319
1874
  });
1320
1875
  }
1321
1876
  }
1877
+ // Pattern 2: (function() { ... }())
1878
+ if (t.isParenthesizedExpression && t.isParenthesizedExpression(expr)) {
1879
+ const inner = expr.expression;
1880
+ if (t.isCallExpression(inner)) {
1881
+ const callee = inner.callee;
1882
+ if (t.isFunctionExpression(callee) || t.isArrowFunctionExpression(callee)) {
1883
+ violations.push({
1884
+ rule: 'no-iife-wrapper',
1885
+ severity: 'critical',
1886
+ line: statement.loc?.start.line || 0,
1887
+ column: statement.loc?.start.column || 0,
1888
+ message: `Component code must not be wrapped in an IIFE. Define the component function directly.`,
1889
+ code: statement.toString().substring(0, 50) + '...'
1890
+ });
1891
+ }
1892
+ }
1893
+ }
1894
+ }
1895
+ // Also check for variable assignment with IIFE
1896
+ if (t.isVariableDeclaration(statement)) {
1897
+ for (const decl of statement.declarations) {
1898
+ if (decl.init && t.isCallExpression(decl.init)) {
1899
+ const callee = decl.init.callee;
1900
+ if (t.isFunctionExpression(callee) || t.isArrowFunctionExpression(callee)) {
1901
+ violations.push({
1902
+ rule: 'no-iife-wrapper',
1903
+ severity: 'critical',
1904
+ line: decl.loc?.start.line || 0,
1905
+ column: decl.loc?.start.column || 0,
1906
+ message: `Do not use IIFE pattern for component initialization. Define components as plain functions.`,
1907
+ code: decl.toString().substring(0, 50) + '...'
1908
+ });
1909
+ }
1910
+ }
1911
+ }
1322
1912
  }
1323
1913
  }
1324
- });
1914
+ }
1325
1915
  return violations;
1326
1916
  }
1327
1917
  },
1328
1918
  {
1329
- name: 'array-reduce-safety',
1919
+ name: 'no-use-reducer',
1330
1920
  appliesTo: 'all',
1331
1921
  test: (ast, componentName, componentSpec) => {
1332
1922
  const violations = [];
1333
1923
  (0, traverse_1.default)(ast, {
1334
1924
  CallExpression(path) {
1335
- // Check for .reduce() calls
1336
- if (t.isMemberExpression(path.node.callee) &&
1337
- t.isIdentifier(path.node.callee.property) &&
1338
- path.node.callee.property.name === 'reduce') {
1339
- // Check if the array might be empty
1340
- const arrayExpression = path.node.callee.object;
1341
- const code = path.toString();
1342
- // Look for patterns that suggest no safety check
1343
- const hasInitialValue = path.node.arguments.length > 1;
1344
- if (!hasInitialValue) {
1345
- violations.push({
1346
- rule: 'array-reduce-safety',
1347
- severity: 'low',
1348
- line: path.node.loc?.start.line || 0,
1349
- column: path.node.loc?.start.column || 0,
1350
- message: `reduce() without initial value may fail on empty arrays: ${code}`,
1351
- code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
1352
- });
1353
- }
1354
- // Check for reduce on array access like arr[0].reduce()
1355
- if (t.isMemberExpression(arrayExpression) &&
1356
- (t.isNumericLiteral(arrayExpression.property) ||
1357
- (t.isIdentifier(arrayExpression.property) && arrayExpression.computed))) {
1358
- violations.push({
1359
- rule: 'array-reduce-safety',
1360
- severity: 'critical',
1361
- line: path.node.loc?.start.line || 0,
1362
- column: path.node.loc?.start.column || 0,
1363
- message: `reduce() on array element access is unsafe: ${code}`,
1364
- code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
1365
- });
1366
- }
1367
- }
1368
- }
1369
- });
1370
- return violations;
1371
- }
1372
- },
1373
- // {
1374
- // name: 'parent-event-callback-usage',
1375
- // appliesTo: 'child',
1925
+ const callee = path.node.callee;
1926
+ if ((t.isIdentifier(callee) && callee.name === 'useReducer') ||
1927
+ (t.isMemberExpression(callee) &&
1928
+ t.isIdentifier(callee.object) && callee.object.name === 'React' &&
1929
+ t.isIdentifier(callee.property) && callee.property.name === 'useReducer')) {
1930
+ violations.push({
1931
+ rule: 'no-use-reducer',
1932
+ severity: 'high', // High but not critical - it's a pattern violation
1933
+ line: path.node.loc?.start.line || 0,
1934
+ column: path.node.loc?.start.column || 0,
1935
+ message: `Component "${componentName}" uses useReducer at line ${path.node.loc?.start.line}. Components should manage state with useState and persist important settings with onSaveUserSettings.`,
1936
+ code: path.toString()
1937
+ });
1938
+ }
1939
+ }
1940
+ });
1941
+ return violations;
1942
+ }
1943
+ },
1944
+ // New rules for the controlled component pattern
1945
+ {
1946
+ name: 'no-data-prop',
1947
+ appliesTo: 'all',
1948
+ test: (ast, componentName, componentSpec) => {
1949
+ const violations = [];
1950
+ (0, traverse_1.default)(ast, {
1951
+ // Check function parameters for 'data' prop
1952
+ FunctionDeclaration(path) {
1953
+ if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
1954
+ const param = path.node.params[0];
1955
+ if (t.isObjectPattern(param)) {
1956
+ for (const prop of param.properties) {
1957
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1958
+ violations.push({
1959
+ rule: 'no-data-prop',
1960
+ severity: 'medium', // It's a pattern issue, not critical
1961
+ line: prop.loc?.start.line || 0,
1962
+ column: prop.loc?.start.column || 0,
1963
+ message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
1964
+ code: 'data prop in component signature'
1965
+ });
1966
+ }
1967
+ }
1968
+ }
1969
+ }
1970
+ },
1971
+ // Also check arrow functions
1972
+ VariableDeclarator(path) {
1973
+ if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
1974
+ const init = path.node.init;
1975
+ if (t.isArrowFunctionExpression(init) && init.params[0]) {
1976
+ const param = init.params[0];
1977
+ if (t.isObjectPattern(param)) {
1978
+ for (const prop of param.properties) {
1979
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1980
+ violations.push({
1981
+ rule: 'no-data-prop',
1982
+ severity: 'critical',
1983
+ line: prop.loc?.start.line || 0,
1984
+ column: prop.loc?.start.column || 0,
1985
+ message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
1986
+ code: 'data prop in component signature'
1987
+ });
1988
+ }
1989
+ }
1990
+ }
1991
+ }
1992
+ }
1993
+ }
1994
+ });
1995
+ return violations;
1996
+ }
1997
+ },
1998
+ {
1999
+ name: 'saved-user-settings-pattern',
2000
+ appliesTo: 'all',
2001
+ test: (ast, componentName, componentSpec) => {
2002
+ const violations = [];
2003
+ // Check for improper onSaveUserSettings usage
2004
+ (0, traverse_1.default)(ast, {
2005
+ CallExpression(path) {
2006
+ const callee = path.node.callee;
2007
+ // Check for onSaveUserSettings calls
2008
+ if (t.isMemberExpression(callee) &&
2009
+ t.isIdentifier(callee.object) && callee.object.name === 'onSaveUserSettings') {
2010
+ // Check if saving ephemeral state
2011
+ if (path.node.arguments.length > 0) {
2012
+ const arg = path.node.arguments[0];
2013
+ if (t.isObjectExpression(arg)) {
2014
+ for (const prop of arg.properties) {
2015
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2016
+ const key = prop.key.name;
2017
+ const ephemeralPatterns = ['hover', 'dropdown', 'modal', 'loading', 'typing', 'draft', 'expanded', 'collapsed', 'focused'];
2018
+ if (ephemeralPatterns.some(pattern => key.toLowerCase().includes(pattern))) {
2019
+ violations.push({
2020
+ rule: 'saved-user-settings-pattern',
2021
+ severity: 'medium', // Pattern issue but not breaking
2022
+ line: prop.loc?.start.line || 0,
2023
+ column: prop.loc?.start.column || 0,
2024
+ message: `Saving ephemeral UI state "${key}" to savedUserSettings. Only save important user preferences.`
2025
+ });
2026
+ }
2027
+ }
2028
+ }
2029
+ }
2030
+ }
2031
+ }
2032
+ }
2033
+ });
2034
+ return violations;
2035
+ }
2036
+ },
2037
+ {
2038
+ name: 'library-variable-names',
2039
+ appliesTo: 'all',
2040
+ test: (ast, componentName, componentSpec) => {
2041
+ const violations = [];
2042
+ // Build a map of library names to their globalVariables
2043
+ const libraryGlobals = new Map();
2044
+ if (componentSpec?.libraries) {
2045
+ for (const lib of componentSpec.libraries) {
2046
+ // Store both the exact name and lowercase for comparison
2047
+ libraryGlobals.set(lib.name.toLowerCase(), lib.globalVariable);
2048
+ }
2049
+ }
2050
+ (0, traverse_1.default)(ast, {
2051
+ VariableDeclarator(path) {
2052
+ // Check for destructuring from a variable (library global)
2053
+ if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
2054
+ const sourceVar = path.node.init.name;
2055
+ // Check if this looks like a library name (case-insensitive match)
2056
+ const matchedLib = Array.from(libraryGlobals.entries()).find(([libName, globalVar]) => sourceVar.toLowerCase() === libName ||
2057
+ sourceVar.toLowerCase() === globalVar.toLowerCase());
2058
+ if (matchedLib) {
2059
+ const [libName, correctGlobal] = matchedLib;
2060
+ if (sourceVar !== correctGlobal) {
2061
+ violations.push({
2062
+ rule: 'library-variable-names',
2063
+ severity: 'critical',
2064
+ line: path.node.loc?.start.line || 0,
2065
+ column: path.node.loc?.start.column || 0,
2066
+ message: `Incorrect library global variable "${sourceVar}". Use the exact globalVariable from the library spec: "${correctGlobal}". Change "const { ... } = ${sourceVar};" to "const { ... } = ${correctGlobal};"`
2067
+ });
2068
+ }
2069
+ }
2070
+ }
2071
+ // Check for self-assignment (const chroma = chroma)
2072
+ if (t.isIdentifier(path.node.id) && t.isIdentifier(path.node.init)) {
2073
+ const idName = path.node.id.name;
2074
+ const initName = path.node.init.name;
2075
+ if (idName === initName) {
2076
+ // Check if this is a library global
2077
+ const isLibraryGlobal = Array.from(libraryGlobals.values()).some(global => global === idName);
2078
+ if (isLibraryGlobal) {
2079
+ violations.push({
2080
+ rule: 'library-variable-names',
2081
+ severity: 'critical',
2082
+ line: path.node.loc?.start.line || 0,
2083
+ column: path.node.loc?.start.column || 0,
2084
+ message: `Self-assignment of library global "${idName}". This variable is already available as a global from the library. Remove this line entirely - the library global is already accessible.`
2085
+ });
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ });
2091
+ return violations;
2092
+ }
2093
+ },
2094
+ {
2095
+ name: 'pass-standard-props',
2096
+ appliesTo: 'all',
2097
+ test: (ast, componentName, componentSpec) => {
2098
+ const violations = [];
2099
+ const requiredProps = ['styles', 'utilities', 'components'];
2100
+ // ONLY check components that are explicitly in our dependencies
2101
+ // Do NOT check library components, HTML elements, or anything else
2102
+ const ourComponentNames = new Set();
2103
+ // Only add components from the componentSpec.dependencies array
2104
+ if (componentSpec?.dependencies && Array.isArray(componentSpec.dependencies)) {
2105
+ for (const dep of componentSpec.dependencies) {
2106
+ if (dep && dep.name) {
2107
+ ourComponentNames.add(dep.name);
2108
+ }
2109
+ }
2110
+ }
2111
+ // If there are no dependencies, skip this rule entirely
2112
+ if (ourComponentNames.size === 0) {
2113
+ return violations;
2114
+ }
2115
+ // Now check only our dependency components for standard props
2116
+ (0, traverse_1.default)(ast, {
2117
+ JSXElement(path) {
2118
+ const openingElement = path.node.openingElement;
2119
+ // Only check if it's one of our dependency components
2120
+ if (t.isJSXIdentifier(openingElement.name)) {
2121
+ const elementName = openingElement.name.name;
2122
+ // CRITICAL: Only check if this component is in our dependencies
2123
+ // Skip all library components (like TableHead, PieChart, etc.)
2124
+ // Skip all HTML elements
2125
+ if (!ourComponentNames.has(elementName)) {
2126
+ return; // Skip this element - it's not one of our dependencies
2127
+ }
2128
+ const passedProps = new Set();
2129
+ // Collect all props being passed
2130
+ for (const attr of openingElement.attributes) {
2131
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
2132
+ passedProps.add(attr.name.name);
2133
+ }
2134
+ }
2135
+ // Check if required props are missing
2136
+ const missingProps = requiredProps.filter(prop => !passedProps.has(prop));
2137
+ if (missingProps.length > 0) {
2138
+ violations.push({
2139
+ rule: 'pass-standard-props',
2140
+ severity: 'critical',
2141
+ line: openingElement.loc?.start.line || 0,
2142
+ column: openingElement.loc?.start.column || 0,
2143
+ message: `Dependency component "${elementName}" is missing required props: ${missingProps.join(', ')}. Components from dependencies must receive styles, utilities, and components props.`,
2144
+ code: `<${elementName} ... />`
2145
+ });
2146
+ }
2147
+ }
2148
+ }
2149
+ });
2150
+ return violations;
2151
+ }
2152
+ },
2153
+ {
2154
+ name: 'no-child-implementation',
2155
+ appliesTo: 'root',
2156
+ test: (ast, componentName, componentSpec) => {
2157
+ const violations = [];
2158
+ const rootFunctionName = componentName;
2159
+ const declaredFunctions = [];
2160
+ // First pass: collect all function declarations
2161
+ (0, traverse_1.default)(ast, {
2162
+ FunctionDeclaration(path) {
2163
+ if (path.node.id) {
2164
+ declaredFunctions.push(path.node.id.name);
2165
+ }
2166
+ }
2167
+ });
2168
+ // If there are multiple function declarations and they look like components
2169
+ // (start with capital letter), it's likely implementing children
2170
+ const componentFunctions = declaredFunctions.filter(name => name !== rootFunctionName && /^[A-Z]/.test(name));
2171
+ if (componentFunctions.length > 0) {
2172
+ violations.push({
2173
+ rule: 'no-child-implementation',
2174
+ severity: 'critical',
2175
+ line: 1,
2176
+ column: 0,
2177
+ message: `Root component file contains child component implementations: ${componentFunctions.join(', ')}. Root should only reference child components, not implement them.`,
2178
+ });
2179
+ }
2180
+ return violations;
2181
+ }
2182
+ },
2183
+ {
2184
+ name: 'undefined-component-usage',
2185
+ appliesTo: 'all',
2186
+ test: (ast, componentName, componentSpec) => {
2187
+ const violations = [];
2188
+ const componentsFromProps = new Set();
2189
+ const componentsUsedInJSX = new Set();
2190
+ let hasComponentsProp = false;
2191
+ (0, traverse_1.default)(ast, {
2192
+ // First, find what's destructured from the components prop
2193
+ VariableDeclarator(path) {
2194
+ if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
2195
+ // Check if destructuring from 'components'
2196
+ if (path.node.init.name === 'components') {
2197
+ hasComponentsProp = true;
2198
+ for (const prop of path.node.id.properties) {
2199
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2200
+ componentsFromProps.add(prop.key.name);
2201
+ }
2202
+ }
2203
+ }
2204
+ }
2205
+ },
2206
+ // Also check object destructuring in function parameters
2207
+ FunctionDeclaration(path) {
2208
+ if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
2209
+ const param = path.node.params[0];
2210
+ if (t.isObjectPattern(param)) {
2211
+ for (const prop of param.properties) {
2212
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'components') {
2213
+ hasComponentsProp = true;
2214
+ // Look for nested destructuring like { components: { A, B } }
2215
+ if (t.isObjectPattern(prop.value)) {
2216
+ for (const innerProp of prop.value.properties) {
2217
+ if (t.isObjectProperty(innerProp) && t.isIdentifier(innerProp.key)) {
2218
+ componentsFromProps.add(innerProp.key.name);
2219
+ }
2220
+ }
2221
+ }
2222
+ }
2223
+ }
2224
+ }
2225
+ }
2226
+ },
2227
+ // Track JSX element usage
2228
+ JSXElement(path) {
2229
+ const openingElement = path.node.openingElement;
2230
+ if (t.isJSXIdentifier(openingElement.name) && /^[A-Z]/.test(openingElement.name.name)) {
2231
+ const componentName = openingElement.name.name;
2232
+ // Only track if it's from our destructured components
2233
+ if (componentsFromProps.has(componentName)) {
2234
+ componentsUsedInJSX.add(componentName);
2235
+ }
2236
+ }
2237
+ }
2238
+ });
2239
+ // Only check if we found a components prop
2240
+ if (hasComponentsProp && componentsFromProps.size > 0) {
2241
+ // Find components that are destructured but never used
2242
+ const unusedComponents = Array.from(componentsFromProps).filter(comp => !componentsUsedInJSX.has(comp));
2243
+ if (unusedComponents.length > 0) {
2244
+ violations.push({
2245
+ rule: 'undefined-component-usage',
2246
+ severity: 'low',
2247
+ line: 1,
2248
+ column: 0,
2249
+ message: `Component destructures ${unusedComponents.join(', ')} from components prop but never uses them. These may be missing from the component spec's dependencies array.`
2250
+ });
2251
+ }
2252
+ }
2253
+ return violations;
2254
+ }
2255
+ },
2256
+ {
2257
+ name: 'unsafe-array-access',
2258
+ appliesTo: 'all',
2259
+ test: (ast, componentName, componentSpec) => {
2260
+ const violations = [];
2261
+ (0, traverse_1.default)(ast, {
2262
+ MemberExpression(path) {
2263
+ // Check for array[index] patterns
2264
+ if (t.isNumericLiteral(path.node.property) ||
2265
+ (t.isIdentifier(path.node.property) && path.node.computed && /^\d+$/.test(path.node.property.name))) {
2266
+ // Look for patterns like: someArray[0].method()
2267
+ const parent = path.parent;
2268
+ if (t.isMemberExpression(parent) && parent.object === path.node) {
2269
+ const code = path.toString();
2270
+ // Check if it's an array access followed by a method call
2271
+ if (/\[\d+\]\.\w+/.test(code)) {
2272
+ violations.push({
2273
+ rule: 'unsafe-array-access',
2274
+ severity: 'critical',
2275
+ line: path.node.loc?.start.line || 0,
2276
+ column: path.node.loc?.start.column || 0,
2277
+ message: `Unsafe array access: ${code}. Check array bounds before accessing elements.`,
2278
+ code: code
2279
+ });
2280
+ }
2281
+ }
2282
+ }
2283
+ }
2284
+ });
2285
+ return violations;
2286
+ }
2287
+ },
2288
+ {
2289
+ name: 'array-reduce-safety',
2290
+ appliesTo: 'all',
2291
+ test: (ast, componentName, componentSpec) => {
2292
+ const violations = [];
2293
+ (0, traverse_1.default)(ast, {
2294
+ CallExpression(path) {
2295
+ // Check for .reduce() calls
2296
+ if (t.isMemberExpression(path.node.callee) &&
2297
+ t.isIdentifier(path.node.callee.property) &&
2298
+ path.node.callee.property.name === 'reduce') {
2299
+ // Check if the array might be empty
2300
+ const arrayExpression = path.node.callee.object;
2301
+ const code = path.toString();
2302
+ // Look for patterns that suggest no safety check
2303
+ const hasInitialValue = path.node.arguments.length > 1;
2304
+ if (!hasInitialValue) {
2305
+ violations.push({
2306
+ rule: 'array-reduce-safety',
2307
+ severity: 'low',
2308
+ line: path.node.loc?.start.line || 0,
2309
+ column: path.node.loc?.start.column || 0,
2310
+ message: `reduce() without initial value may fail on empty arrays: ${code}`,
2311
+ code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
2312
+ });
2313
+ }
2314
+ // Check for reduce on array access like arr[0].reduce()
2315
+ if (t.isMemberExpression(arrayExpression) &&
2316
+ (t.isNumericLiteral(arrayExpression.property) ||
2317
+ (t.isIdentifier(arrayExpression.property) && arrayExpression.computed))) {
2318
+ violations.push({
2319
+ rule: 'array-reduce-safety',
2320
+ severity: 'critical',
2321
+ line: path.node.loc?.start.line || 0,
2322
+ column: path.node.loc?.start.column || 0,
2323
+ message: `reduce() on array element access is unsafe: ${code}`,
2324
+ code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
2325
+ });
2326
+ }
2327
+ }
2328
+ }
2329
+ });
2330
+ return violations;
2331
+ }
2332
+ },
2333
+ // {
2334
+ // name: 'parent-event-callback-usage',
2335
+ // appliesTo: 'child',
1376
2336
  // test: (ast: t.File, componentName: string) => {
1377
2337
  // const violations: Violation[] = [];
1378
2338
  // const eventCallbacks = new Map<string, { line: number; column: number }>();
@@ -1505,152 +2465,716 @@ ComponentLinter.universalComponentRules = [
1505
2465
  // }
1506
2466
  // },
1507
2467
  {
1508
- name: 'property-name-consistency',
2468
+ name: 'property-name-consistency',
2469
+ appliesTo: 'all',
2470
+ test: (ast, componentName, componentSpec) => {
2471
+ const violations = [];
2472
+ const dataTransformations = new Map();
2473
+ const propertyAccesses = new Map(); // variable -> accessed properties
2474
+ // Track data transformations (especially in map functions)
2475
+ (0, traverse_1.default)(ast, {
2476
+ CallExpression(path) {
2477
+ // Look for array.map transformations
2478
+ if (t.isMemberExpression(path.node.callee) &&
2479
+ t.isIdentifier(path.node.callee.property) &&
2480
+ path.node.callee.property.name === 'map') {
2481
+ const mapArg = path.node.arguments[0];
2482
+ if (mapArg && (t.isArrowFunctionExpression(mapArg) || t.isFunctionExpression(mapArg))) {
2483
+ const param = mapArg.params[0];
2484
+ if (t.isIdentifier(param)) {
2485
+ const paramName = param.name;
2486
+ const originalProps = new Set();
2487
+ const transformedProps = new Set();
2488
+ // Check the return value
2489
+ let returnValue = null;
2490
+ if (t.isArrowFunctionExpression(mapArg)) {
2491
+ if (t.isObjectExpression(mapArg.body)) {
2492
+ returnValue = mapArg.body;
2493
+ }
2494
+ else if (t.isBlockStatement(mapArg.body)) {
2495
+ // Find return statement
2496
+ for (const stmt of mapArg.body.body) {
2497
+ if (t.isReturnStatement(stmt) && stmt.argument) {
2498
+ returnValue = stmt.argument;
2499
+ break;
2500
+ }
2501
+ }
2502
+ }
2503
+ }
2504
+ // Analyze object mapping
2505
+ if (returnValue && t.isObjectExpression(returnValue)) {
2506
+ for (const prop of returnValue.properties) {
2507
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2508
+ transformedProps.add(prop.key.name);
2509
+ // Check if value is a member expression from the parameter
2510
+ if (t.isMemberExpression(prop.value) &&
2511
+ t.isIdentifier(prop.value.object) &&
2512
+ prop.value.object.name === paramName &&
2513
+ t.isIdentifier(prop.value.property)) {
2514
+ originalProps.add(prop.value.property.name);
2515
+ }
2516
+ }
2517
+ }
2518
+ }
2519
+ // Store the transformation if we found property mappings
2520
+ if (transformedProps.size > 0) {
2521
+ // Find the variable being assigned
2522
+ let parentPath = path.parentPath;
2523
+ while (parentPath && !t.isVariableDeclarator(parentPath.node) && !t.isCallExpression(parentPath.node)) {
2524
+ parentPath = parentPath.parentPath;
2525
+ }
2526
+ if (parentPath && t.isCallExpression(parentPath.node)) {
2527
+ // Check for setState calls
2528
+ if (t.isIdentifier(parentPath.node.callee) && /^set[A-Z]/.test(parentPath.node.callee.name)) {
2529
+ const stateName = parentPath.node.callee.name.replace(/^set/, '');
2530
+ const varName = stateName.charAt(0).toLowerCase() + stateName.slice(1);
2531
+ dataTransformations.set(varName, {
2532
+ originalProps,
2533
+ transformedProps,
2534
+ location: {
2535
+ line: path.node.loc?.start.line || 0,
2536
+ column: path.node.loc?.start.column || 0
2537
+ }
2538
+ });
2539
+ }
2540
+ }
2541
+ }
2542
+ }
2543
+ }
2544
+ }
2545
+ },
2546
+ // Track property accesses
2547
+ MemberExpression(path) {
2548
+ if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
2549
+ const objName = path.node.object.name;
2550
+ const propName = path.node.property.name;
2551
+ if (!propertyAccesses.has(objName)) {
2552
+ propertyAccesses.set(objName, new Set());
2553
+ }
2554
+ propertyAccesses.get(objName).add(propName);
2555
+ }
2556
+ }
2557
+ });
2558
+ // Check for mismatches
2559
+ for (const [varName, transformation] of dataTransformations) {
2560
+ const accesses = propertyAccesses.get(varName);
2561
+ if (accesses) {
2562
+ for (const accessedProp of accesses) {
2563
+ // Check if accessed property exists in transformed props
2564
+ if (!transformation.transformedProps.has(accessedProp)) {
2565
+ // Check if it's trying to use original prop name
2566
+ const matchingOriginal = Array.from(transformation.originalProps).find(orig => orig.toLowerCase() === accessedProp.toLowerCase());
2567
+ if (matchingOriginal) {
2568
+ // Find the transformed name
2569
+ const transformedName = Array.from(transformation.transformedProps).find(t => t.toLowerCase() === accessedProp.toLowerCase());
2570
+ violations.push({
2571
+ rule: 'property-name-consistency',
2572
+ severity: 'critical',
2573
+ line: transformation.location.line,
2574
+ column: transformation.location.column,
2575
+ message: `Property name mismatch: data transformed with different casing. Accessing '${accessedProp}' but property was transformed to '${transformedName || 'different name'}'`,
2576
+ code: `Transform uses '${Array.from(transformation.transformedProps).join(', ')}' but code accesses '${accessedProp}'`
2577
+ });
2578
+ }
2579
+ }
2580
+ }
2581
+ }
2582
+ }
2583
+ return violations;
2584
+ }
2585
+ },
2586
+ // New rules to align with AI linter
2587
+ {
2588
+ name: 'noisy-settings-updates',
1509
2589
  appliesTo: 'all',
1510
2590
  test: (ast, componentName, componentSpec) => {
1511
2591
  const violations = [];
1512
- const dataTransformations = new Map();
1513
- const propertyAccesses = new Map(); // variable -> accessed properties
1514
- // Track data transformations (especially in map functions)
1515
2592
  (0, traverse_1.default)(ast, {
1516
2593
  CallExpression(path) {
1517
- // Look for array.map transformations
1518
- if (t.isMemberExpression(path.node.callee) &&
1519
- t.isIdentifier(path.node.callee.property) &&
1520
- path.node.callee.property.name === 'map') {
1521
- const mapArg = path.node.arguments[0];
1522
- if (mapArg && (t.isArrowFunctionExpression(mapArg) || t.isFunctionExpression(mapArg))) {
1523
- const param = mapArg.params[0];
1524
- if (t.isIdentifier(param)) {
1525
- const paramName = param.name;
1526
- const originalProps = new Set();
1527
- const transformedProps = new Set();
1528
- // Check the return value
1529
- let returnValue = null;
1530
- if (t.isArrowFunctionExpression(mapArg)) {
1531
- if (t.isObjectExpression(mapArg.body)) {
1532
- returnValue = mapArg.body;
2594
+ // Check for onSaveUserSettings calls
2595
+ if (t.isOptionalCallExpression(path.node) || t.isCallExpression(path.node)) {
2596
+ const callee = path.node.callee;
2597
+ if (t.isIdentifier(callee) && callee.name === 'onSaveUserSettings') {
2598
+ // Check if this is inside an onChange/onInput handler
2599
+ let parent = path.getFunctionParent();
2600
+ if (parent) {
2601
+ const funcName = ComponentLinter.getFunctionName(parent);
2602
+ if (funcName && (funcName.includes('Change') || funcName.includes('Input'))) {
2603
+ // Check if it's not debounced or on blur
2604
+ const parentBody = parent.node.body;
2605
+ const hasDebounce = parentBody && parentBody.toString().includes('debounce');
2606
+ const hasTimeout = parentBody && parentBody.toString().includes('setTimeout');
2607
+ if (!hasDebounce && !hasTimeout) {
2608
+ violations.push({
2609
+ rule: 'noisy-settings-updates',
2610
+ severity: 'critical',
2611
+ line: path.node.loc?.start.line || 0,
2612
+ column: path.node.loc?.start.column || 0,
2613
+ message: `Saving settings on every change/keystroke. Save on blur, submit, or after debouncing.`
2614
+ });
1533
2615
  }
1534
- else if (t.isBlockStatement(mapArg.body)) {
1535
- // Find return statement
1536
- for (const stmt of mapArg.body.body) {
1537
- if (t.isReturnStatement(stmt) && stmt.argument) {
1538
- returnValue = stmt.argument;
1539
- break;
1540
- }
2616
+ }
2617
+ }
2618
+ }
2619
+ }
2620
+ }
2621
+ });
2622
+ return violations;
2623
+ }
2624
+ },
2625
+ {
2626
+ name: 'prop-state-sync',
2627
+ appliesTo: 'all',
2628
+ test: (ast, componentName, componentSpec) => {
2629
+ const violations = [];
2630
+ (0, traverse_1.default)(ast, {
2631
+ CallExpression(path) {
2632
+ if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useEffect') {
2633
+ const effectBody = path.node.arguments[0];
2634
+ const deps = path.node.arguments[1];
2635
+ if (effectBody && (t.isArrowFunctionExpression(effectBody) || t.isFunctionExpression(effectBody))) {
2636
+ const bodyString = effectBody.body.toString();
2637
+ // Check if it's setting state based on props
2638
+ const hasSetState = /set[A-Z]\w*\s*\(/.test(bodyString);
2639
+ const depsString = deps ? deps.toString() : '';
2640
+ // Check if deps include prop-like names
2641
+ const propPatterns = ['Prop', 'value', 'data', 'items'];
2642
+ const hasPropDeps = propPatterns.some(p => depsString.includes(p));
2643
+ if (hasSetState && hasPropDeps && !bodyString.includes('async')) {
2644
+ violations.push({
2645
+ rule: 'prop-state-sync',
2646
+ severity: 'critical',
2647
+ line: path.node.loc?.start.line || 0,
2648
+ column: path.node.loc?.start.column || 0,
2649
+ message: 'Syncing props to internal state with useEffect creates dual state management',
2650
+ code: path.toString().substring(0, 100)
2651
+ });
2652
+ }
2653
+ }
2654
+ }
2655
+ }
2656
+ });
2657
+ return violations;
2658
+ }
2659
+ },
2660
+ {
2661
+ name: 'performance-memoization',
2662
+ appliesTo: 'all',
2663
+ test: (ast, componentName, componentSpec) => {
2664
+ const violations = [];
2665
+ const memoizedValues = new Set();
2666
+ // Collect memoized values
2667
+ (0, traverse_1.default)(ast, {
2668
+ CallExpression(path) {
2669
+ if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useMemo') {
2670
+ // Find the variable being assigned
2671
+ if (t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) {
2672
+ memoizedValues.add(path.parent.id.name);
2673
+ }
2674
+ }
2675
+ }
2676
+ });
2677
+ // Check for expensive operations without memoization
2678
+ (0, traverse_1.default)(ast, {
2679
+ CallExpression(path) {
2680
+ if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
2681
+ const method = path.node.callee.property.name;
2682
+ // Check for expensive array operations
2683
+ if (['filter', 'sort', 'map', 'reduce'].includes(method)) {
2684
+ // Check if this is inside a variable declaration
2685
+ let parentPath = path.parentPath;
2686
+ while (parentPath && !t.isVariableDeclarator(parentPath.node)) {
2687
+ parentPath = parentPath.parentPath;
2688
+ }
2689
+ if (parentPath && t.isVariableDeclarator(parentPath.node) && t.isIdentifier(parentPath.node.id)) {
2690
+ const varName = parentPath.node.id.name;
2691
+ // Check if it's not memoized
2692
+ if (!memoizedValues.has(varName)) {
2693
+ // Check if it's in the render method (not in event handlers)
2694
+ let funcParent = path.getFunctionParent();
2695
+ if (funcParent) {
2696
+ const funcName = ComponentLinter.getFunctionName(funcParent);
2697
+ if (!funcName || funcName === componentName) {
2698
+ violations.push({
2699
+ rule: 'performance-memoization',
2700
+ severity: 'low', // Just a suggestion, not mandatory
2701
+ line: path.node.loc?.start.line || 0,
2702
+ column: path.node.loc?.start.column || 0,
2703
+ message: `Expensive ${method} operation without memoization. Consider using useMemo.`,
2704
+ code: `const ${varName} = ...${method}(...)`
2705
+ });
1541
2706
  }
1542
2707
  }
1543
2708
  }
1544
- // Analyze object mapping
1545
- if (returnValue && t.isObjectExpression(returnValue)) {
1546
- for (const prop of returnValue.properties) {
1547
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1548
- transformedProps.add(prop.key.name);
1549
- // Check if value is a member expression from the parameter
1550
- if (t.isMemberExpression(prop.value) &&
1551
- t.isIdentifier(prop.value.object) &&
1552
- prop.value.object.name === paramName &&
1553
- t.isIdentifier(prop.value.property)) {
1554
- originalProps.add(prop.value.property.name);
1555
- }
1556
- }
1557
- }
2709
+ }
2710
+ }
2711
+ }
2712
+ },
2713
+ // Check for static arrays/objects
2714
+ VariableDeclarator(path) {
2715
+ if (t.isIdentifier(path.node.id) &&
2716
+ (t.isArrayExpression(path.node.init) || t.isObjectExpression(path.node.init))) {
2717
+ const varName = path.node.id.name;
2718
+ if (!memoizedValues.has(varName)) {
2719
+ // Check if it looks static (no variables referenced)
2720
+ const hasVariables = path.node.init.toString().match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g);
2721
+ if (!hasVariables || hasVariables.length < 3) { // Allow some property names
2722
+ violations.push({
2723
+ rule: 'performance-memoization',
2724
+ severity: 'low', // Just a suggestion
2725
+ line: path.node.loc?.start.line || 0,
2726
+ column: path.node.loc?.start.column || 0,
2727
+ message: 'Static array/object recreated on every render. Consider using useMemo.',
2728
+ code: `const ${varName} = ${path.node.init.type === 'ArrayExpression' ? '[...]' : '{...}'}`
2729
+ });
2730
+ }
2731
+ }
2732
+ }
2733
+ }
2734
+ });
2735
+ return violations;
2736
+ }
2737
+ },
2738
+ {
2739
+ name: 'child-state-management',
2740
+ appliesTo: 'all',
2741
+ test: (ast, componentName, componentSpec) => {
2742
+ const violations = [];
2743
+ (0, traverse_1.default)(ast, {
2744
+ CallExpression(path) {
2745
+ if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useState') {
2746
+ // Check if the state name suggests child component state
2747
+ if (t.isVariableDeclarator(path.parent) && t.isArrayPattern(path.parent.id)) {
2748
+ const stateNameNode = path.parent.id.elements[0];
2749
+ if (t.isIdentifier(stateNameNode)) {
2750
+ const stateName = stateNameNode.name;
2751
+ // Check for patterns suggesting child state management
2752
+ const childPatterns = [
2753
+ /^child/i,
2754
+ /Table\w*State/,
2755
+ /Panel\w*State/,
2756
+ /Modal\w*State/,
2757
+ /\w+Component\w*/
2758
+ ];
2759
+ if (childPatterns.some(pattern => pattern.test(stateName))) {
2760
+ violations.push({
2761
+ rule: 'child-state-management',
2762
+ severity: 'critical',
2763
+ line: path.node.loc?.start.line || 0,
2764
+ column: path.node.loc?.start.column || 0,
2765
+ message: `Component trying to manage child component state: ${stateName}. Child components manage their own state!`,
2766
+ code: `const [${stateName}, ...] = useState(...)`
2767
+ });
2768
+ }
2769
+ }
2770
+ }
2771
+ }
2772
+ }
2773
+ });
2774
+ return violations;
2775
+ }
2776
+ },
2777
+ {
2778
+ name: 'server-reload-on-client-operation',
2779
+ appliesTo: 'all',
2780
+ test: (ast, componentName, componentSpec) => {
2781
+ const violations = [];
2782
+ (0, traverse_1.default)(ast, {
2783
+ CallExpression(path) {
2784
+ const callee = path.node.callee;
2785
+ // Look for data loading functions
2786
+ if (t.isIdentifier(callee) &&
2787
+ (callee.name.includes('load') || callee.name.includes('fetch'))) {
2788
+ // Check if it's called in sort/filter handlers
2789
+ let funcParent = path.getFunctionParent();
2790
+ if (funcParent) {
2791
+ const funcName = ComponentLinter.getFunctionName(funcParent);
2792
+ if (funcName &&
2793
+ (funcName.includes('Sort') || funcName.includes('Filter') ||
2794
+ funcName.includes('handleSort') || funcName.includes('handleFilter'))) {
2795
+ violations.push({
2796
+ rule: 'server-reload-on-client-operation',
2797
+ severity: 'critical',
2798
+ line: path.node.loc?.start.line || 0,
2799
+ column: path.node.loc?.start.column || 0,
2800
+ message: 'Reloading data from server on sort/filter. Use useMemo for client-side operations.',
2801
+ code: `${funcName} calls ${callee.name}`
2802
+ });
2803
+ }
2804
+ }
2805
+ }
2806
+ }
2807
+ });
2808
+ return violations;
2809
+ }
2810
+ },
2811
+ {
2812
+ name: 'runview-runquery-valid-properties',
2813
+ appliesTo: 'all',
2814
+ test: (ast, componentName, componentSpec) => {
2815
+ const violations = [];
2816
+ // Valid properties for RunView/RunViews
2817
+ const validRunViewProps = new Set([
2818
+ 'ViewID', 'ViewName', 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
2819
+ 'MaxRows', 'StartRow', 'ResultType', 'UserSearchString', 'ForceAuditLog', 'AuditLogDescription',
2820
+ 'ResultType'
2821
+ ]);
2822
+ // Valid properties for RunQuery
2823
+ const validRunQueryProps = new Set([
2824
+ 'QueryID', 'QueryName', 'CategoryID', 'CategoryPath', 'Parameters', 'MaxRows', 'StartRow', 'ForceAuditLog', 'AuditLogDescription'
2825
+ ]);
2826
+ (0, traverse_1.default)(ast, {
2827
+ CallExpression(path) {
2828
+ const callee = path.node.callee;
2829
+ // Check for utilities.rv.RunView or utilities.rv.RunViews
2830
+ if (t.isMemberExpression(callee) &&
2831
+ t.isMemberExpression(callee.object) &&
2832
+ t.isIdentifier(callee.object.object) &&
2833
+ callee.object.object.name === 'utilities' &&
2834
+ t.isIdentifier(callee.object.property) &&
2835
+ callee.object.property.name === 'rv' &&
2836
+ t.isIdentifier(callee.property)) {
2837
+ const methodName = callee.property.name;
2838
+ if (methodName === 'RunView' || methodName === 'RunViews') {
2839
+ // Check that first parameter exists
2840
+ if (!path.node.arguments[0]) {
2841
+ violations.push({
2842
+ rule: 'runview-runquery-valid-properties',
2843
+ severity: 'critical',
2844
+ line: path.node.loc?.start.line || 0,
2845
+ column: path.node.loc?.start.column || 0,
2846
+ message: `${methodName} requires a ${methodName === 'RunViews' ? 'array of RunViewParams objects' : 'RunViewParams object'} as the first parameter.`,
2847
+ code: `${methodName}()`
2848
+ });
2849
+ return;
2850
+ }
2851
+ // Get the config object(s)
2852
+ let configs = [];
2853
+ let hasValidFirstParam = false;
2854
+ if (methodName === 'RunViews') {
2855
+ // RunViews takes an array of configs
2856
+ if (t.isArrayExpression(path.node.arguments[0])) {
2857
+ hasValidFirstParam = true;
2858
+ configs = path.node.arguments[0].elements
2859
+ .filter((e) => t.isObjectExpression(e));
2860
+ }
2861
+ else {
2862
+ violations.push({
2863
+ rule: 'runview-runquery-valid-properties',
2864
+ severity: 'critical',
2865
+ line: path.node.arguments[0].loc?.start.line || 0,
2866
+ column: path.node.arguments[0].loc?.start.column || 0,
2867
+ message: `RunViews expects an array of RunViewParams objects, not a ${t.isObjectExpression(path.node.arguments[0]) ? 'single object' : 'non-array'}. Use: RunViews([{ EntityName: 'Entity1' }, { EntityName: 'Entity2' }])`,
2868
+ code: path.toString().substring(0, 100)
2869
+ });
1558
2870
  }
1559
- // Store the transformation if we found property mappings
1560
- if (transformedProps.size > 0) {
1561
- // Find the variable being assigned
1562
- let parentPath = path.parentPath;
1563
- while (parentPath && !t.isVariableDeclarator(parentPath.node) && !t.isCallExpression(parentPath.node)) {
1564
- parentPath = parentPath.parentPath;
1565
- }
1566
- if (parentPath && t.isCallExpression(parentPath.node)) {
1567
- // Check for setState calls
1568
- if (t.isIdentifier(parentPath.node.callee) && /^set[A-Z]/.test(parentPath.node.callee.name)) {
1569
- const stateName = parentPath.node.callee.name.replace(/^set/, '');
1570
- const varName = stateName.charAt(0).toLowerCase() + stateName.slice(1);
1571
- dataTransformations.set(varName, {
1572
- originalProps,
1573
- transformedProps,
1574
- location: {
1575
- line: path.node.loc?.start.line || 0,
1576
- column: path.node.loc?.start.column || 0
1577
- }
2871
+ }
2872
+ else if (methodName === 'RunView') {
2873
+ // RunView takes a single config
2874
+ if (t.isObjectExpression(path.node.arguments[0])) {
2875
+ hasValidFirstParam = true;
2876
+ configs = [path.node.arguments[0]];
2877
+ }
2878
+ else {
2879
+ const argType = t.isStringLiteral(path.node.arguments[0]) ? 'string' :
2880
+ t.isArrayExpression(path.node.arguments[0]) ? 'array' :
2881
+ t.isIdentifier(path.node.arguments[0]) ? 'identifier' :
2882
+ 'non-object';
2883
+ violations.push({
2884
+ rule: 'runview-runquery-valid-properties',
2885
+ severity: 'critical',
2886
+ line: path.node.arguments[0].loc?.start.line || 0,
2887
+ column: path.node.arguments[0].loc?.start.column || 0,
2888
+ message: `RunView expects a RunViewParams object, not ${argType === 'array' ? 'an' : 'a'} ${argType}. Use: RunView({ EntityName: 'YourEntity' }) or for multiple use RunViews([...])`,
2889
+ code: path.toString().substring(0, 100)
2890
+ });
2891
+ }
2892
+ }
2893
+ if (!hasValidFirstParam) {
2894
+ return;
2895
+ }
2896
+ // Check each config for invalid properties and required fields
2897
+ for (const config of configs) {
2898
+ // Check for required properties (must have ViewID, ViewName, ViewEntity, or EntityName)
2899
+ let hasViewID = false;
2900
+ let hasViewName = false;
2901
+ let hasViewEntity = false;
2902
+ let hasEntityName = false;
2903
+ for (const prop of config.properties) {
2904
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2905
+ const propName = prop.key.name;
2906
+ if (propName === 'ViewID')
2907
+ hasViewID = true;
2908
+ if (propName === 'ViewName')
2909
+ hasViewName = true;
2910
+ if (propName === 'ViewEntity')
2911
+ hasViewEntity = true;
2912
+ if (propName === 'EntityName')
2913
+ hasEntityName = true;
2914
+ if (!validRunViewProps.has(propName)) {
2915
+ // Special error messages for common mistakes
2916
+ let message = `Invalid property '${propName}' on ${methodName}. Valid properties: ${Array.from(validRunViewProps).join(', ')}`;
2917
+ let fix = `Remove '${propName}' property`;
2918
+ if (propName === 'Parameters') {
2919
+ message = `${methodName} does not support 'Parameters'. Use 'ExtraFilter' for WHERE clauses.`;
2920
+ fix = `Replace 'Parameters' with 'ExtraFilter' and format as SQL WHERE clause`;
2921
+ }
2922
+ else if (propName === 'GroupBy') {
2923
+ message = `${methodName} does not support 'GroupBy'. Use RunQuery with a pre-defined query for aggregations.`;
2924
+ fix = `Remove 'GroupBy' and use RunQuery instead for aggregated data`;
2925
+ }
2926
+ else if (propName === 'Having') {
2927
+ message = `${methodName} does not support 'Having'. Use RunQuery with a pre-defined query.`;
2928
+ fix = `Remove 'Having' and use RunQuery instead`;
2929
+ }
2930
+ violations.push({
2931
+ rule: 'runview-runquery-valid-properties',
2932
+ severity: 'critical',
2933
+ line: prop.loc?.start.line || 0,
2934
+ column: prop.loc?.start.column || 0,
2935
+ message,
2936
+ code: `${propName}: ...`
1578
2937
  });
1579
2938
  }
1580
2939
  }
1581
2940
  }
2941
+ // Check that at least one required property is present
2942
+ if (!hasViewID && !hasViewName && !hasViewEntity && !hasEntityName) {
2943
+ violations.push({
2944
+ rule: 'runview-runquery-valid-properties',
2945
+ severity: 'critical',
2946
+ line: config.loc?.start.line || 0,
2947
+ column: config.loc?.start.column || 0,
2948
+ message: `${methodName} requires one of: ViewID, ViewName, ViewEntity, or EntityName. Add one to identify what data to retrieve.`,
2949
+ code: `${methodName}({ ... })`
2950
+ });
2951
+ }
1582
2952
  }
1583
2953
  }
1584
2954
  }
1585
- },
1586
- // Track property accesses
1587
- MemberExpression(path) {
1588
- if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
1589
- const objName = path.node.object.name;
1590
- const propName = path.node.property.name;
1591
- if (!propertyAccesses.has(objName)) {
1592
- propertyAccesses.set(objName, new Set());
2955
+ // Check for utilities.rq.RunQuery
2956
+ if (t.isMemberExpression(callee) &&
2957
+ t.isMemberExpression(callee.object) &&
2958
+ t.isIdentifier(callee.object.object) &&
2959
+ callee.object.object.name === 'utilities' &&
2960
+ t.isIdentifier(callee.object.property) &&
2961
+ callee.object.property.name === 'rq' &&
2962
+ t.isIdentifier(callee.property) &&
2963
+ callee.property.name === 'RunQuery') {
2964
+ // Check that first parameter exists and is an object
2965
+ if (!path.node.arguments[0]) {
2966
+ violations.push({
2967
+ rule: 'runview-runquery-valid-properties',
2968
+ severity: 'critical',
2969
+ line: path.node.loc?.start.line || 0,
2970
+ column: path.node.loc?.start.column || 0,
2971
+ message: `RunQuery requires a RunQueryParams object as the first parameter. Must provide an object with either QueryID or QueryName.`,
2972
+ code: `RunQuery()`
2973
+ });
2974
+ }
2975
+ else if (!t.isObjectExpression(path.node.arguments[0])) {
2976
+ // First parameter is not an object
2977
+ const argType = t.isStringLiteral(path.node.arguments[0]) ? 'string' :
2978
+ t.isIdentifier(path.node.arguments[0]) ? 'identifier' :
2979
+ 'non-object';
2980
+ violations.push({
2981
+ rule: 'runview-runquery-valid-properties',
2982
+ severity: 'critical',
2983
+ line: path.node.arguments[0].loc?.start.line || 0,
2984
+ column: path.node.arguments[0].loc?.start.column || 0,
2985
+ message: `RunQuery expects a RunQueryParams object, not a ${argType}. Use: RunQuery({ QueryName: 'YourQuery' }) or RunQuery({ QueryID: 'id' })`,
2986
+ code: path.toString().substring(0, 100)
2987
+ });
2988
+ }
2989
+ else {
2990
+ const config = path.node.arguments[0];
2991
+ // Check for required properties (must have QueryID or QueryName)
2992
+ let hasQueryID = false;
2993
+ let hasQueryName = false;
2994
+ for (const prop of config.properties) {
2995
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2996
+ const propName = prop.key.name;
2997
+ if (propName === 'QueryID')
2998
+ hasQueryID = true;
2999
+ if (propName === 'QueryName')
3000
+ hasQueryName = true;
3001
+ if (!validRunQueryProps.has(propName)) {
3002
+ let message = `Invalid property '${propName}' on RunQuery. Valid properties: ${Array.from(validRunQueryProps).join(', ')}`;
3003
+ let fix = `Remove '${propName}' property`;
3004
+ if (propName === 'ExtraFilter') {
3005
+ message = `RunQuery does not support 'ExtraFilter'. WHERE clauses should be in the pre-defined query or passed as Parameters.`;
3006
+ fix = `Remove 'ExtraFilter'. Add WHERE logic to the query definition or pass as Parameters`;
3007
+ }
3008
+ else if (propName === 'Fields') {
3009
+ message = `RunQuery does not support 'Fields'. The query definition determines returned fields.`;
3010
+ fix = `Remove 'Fields'. Modify the query definition to return desired fields`;
3011
+ }
3012
+ else if (propName === 'OrderBy') {
3013
+ message = `RunQuery does not support 'OrderBy'. ORDER BY should be in the query definition.`;
3014
+ fix = `Remove 'OrderBy'. Add ORDER BY to the query definition`;
3015
+ }
3016
+ violations.push({
3017
+ rule: 'runview-runquery-valid-properties',
3018
+ severity: 'critical',
3019
+ line: prop.loc?.start.line || 0,
3020
+ column: prop.loc?.start.column || 0,
3021
+ message,
3022
+ code: `${propName}: ...`
3023
+ });
3024
+ }
3025
+ }
3026
+ }
3027
+ // Check that at least one required property is present
3028
+ if (!hasQueryID && !hasQueryName) {
3029
+ violations.push({
3030
+ rule: 'runview-runquery-valid-properties',
3031
+ severity: 'critical',
3032
+ line: config.loc?.start.line || 0,
3033
+ column: config.loc?.start.column || 0,
3034
+ message: `RunQuery requires either QueryID or QueryName property. Add one of these to identify the query to run.`,
3035
+ code: `RunQuery({ ... })`
3036
+ });
3037
+ }
1593
3038
  }
1594
- propertyAccesses.get(objName).add(propName);
1595
3039
  }
1596
3040
  }
1597
3041
  });
1598
- // Check for mismatches
1599
- for (const [varName, transformation] of dataTransformations) {
1600
- const accesses = propertyAccesses.get(varName);
1601
- if (accesses) {
1602
- for (const accessedProp of accesses) {
1603
- // Check if accessed property exists in transformed props
1604
- if (!transformation.transformedProps.has(accessedProp)) {
1605
- // Check if it's trying to use original prop name
1606
- const matchingOriginal = Array.from(transformation.originalProps).find(orig => orig.toLowerCase() === accessedProp.toLowerCase());
1607
- if (matchingOriginal) {
1608
- // Find the transformed name
1609
- const transformedName = Array.from(transformation.transformedProps).find(t => t.toLowerCase() === accessedProp.toLowerCase());
3042
+ return violations;
3043
+ }
3044
+ },
3045
+ {
3046
+ name: 'root-component-props-restriction',
3047
+ appliesTo: 'root',
3048
+ test: (ast, componentName, componentSpec) => {
3049
+ const violations = [];
3050
+ const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
3051
+ // This rule applies when testing root components
3052
+ // We can identify this by checking if the component spec indicates it's a root component
3053
+ // For now, we'll apply this rule universally and let the caller decide when to use it
3054
+ (0, traverse_1.default)(ast, {
3055
+ FunctionDeclaration(path) {
3056
+ if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
3057
+ const param = path.node.params[0];
3058
+ if (t.isObjectPattern(param)) {
3059
+ const invalidProps = [];
3060
+ const allProps = [];
3061
+ for (const prop of param.properties) {
3062
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3063
+ const propName = prop.key.name;
3064
+ allProps.push(propName);
3065
+ if (!standardProps.has(propName)) {
3066
+ invalidProps.push(propName);
3067
+ }
3068
+ }
3069
+ }
3070
+ // Only report if there are non-standard props
3071
+ // This allows the rule to be selectively applied to root components
3072
+ if (invalidProps.length > 0) {
1610
3073
  violations.push({
1611
- rule: 'property-name-consistency',
3074
+ rule: 'root-component-props-restriction',
1612
3075
  severity: 'critical',
1613
- line: transformation.location.line,
1614
- column: transformation.location.column,
1615
- message: `Property name mismatch: data transformed with different casing. Accessing '${accessedProp}' but property was transformed to '${transformedName || 'different name'}'`,
1616
- code: `Transform uses '${Array.from(transformation.transformedProps).join(', ')}' but code accesses '${accessedProp}'`
3076
+ line: path.node.loc?.start.line || 0,
3077
+ column: path.node.loc?.start.column || 0,
3078
+ message: `Component "${componentName}" accepts non-standard props: ${invalidProps.join(', ')}. Root components can only accept standard props: ${Array.from(standardProps).join(', ')}. Load data internally using utilities.rv.RunView().`
1617
3079
  });
1618
3080
  }
1619
3081
  }
1620
3082
  }
3083
+ },
3084
+ // Also check arrow function components
3085
+ VariableDeclarator(path) {
3086
+ if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
3087
+ const init = path.node.init;
3088
+ if (t.isArrowFunctionExpression(init) && init.params[0]) {
3089
+ const param = init.params[0];
3090
+ if (t.isObjectPattern(param)) {
3091
+ const invalidProps = [];
3092
+ const allProps = [];
3093
+ for (const prop of param.properties) {
3094
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3095
+ const propName = prop.key.name;
3096
+ allProps.push(propName);
3097
+ if (!standardProps.has(propName)) {
3098
+ invalidProps.push(propName);
3099
+ }
3100
+ }
3101
+ }
3102
+ if (invalidProps.length > 0) {
3103
+ violations.push({
3104
+ rule: 'root-component-props-restriction',
3105
+ severity: 'critical',
3106
+ line: path.node.loc?.start.line || 0,
3107
+ column: path.node.loc?.start.column || 0,
3108
+ message: `Component "${componentName}" accepts non-standard props: ${invalidProps.join(', ')}. Root components can only accept standard props: ${Array.from(standardProps).join(', ')}. Load data internally using utilities.rv.RunView().`
3109
+ });
3110
+ }
3111
+ }
3112
+ }
3113
+ }
1621
3114
  }
1622
- }
3115
+ });
1623
3116
  return violations;
1624
3117
  }
1625
3118
  },
1626
- // New rules to align with AI linter
1627
3119
  {
1628
- name: 'noisy-settings-updates',
3120
+ name: 'invalid-components-destructuring',
1629
3121
  appliesTo: 'all',
1630
3122
  test: (ast, componentName, componentSpec) => {
1631
3123
  const violations = [];
3124
+ // Build sets of valid component names and library names
3125
+ const validComponentNames = new Set();
3126
+ const libraryNames = new Set();
3127
+ const libraryGlobalVars = new Set();
3128
+ // Add dependency components
3129
+ if (componentSpec?.dependencies) {
3130
+ for (const dep of componentSpec.dependencies) {
3131
+ if (dep.name) {
3132
+ validComponentNames.add(dep.name);
3133
+ }
3134
+ }
3135
+ }
3136
+ // Add libraries
3137
+ if (componentSpec?.libraries) {
3138
+ for (const lib of componentSpec.libraries) {
3139
+ if (lib.name) {
3140
+ libraryNames.add(lib.name);
3141
+ }
3142
+ if (lib.globalVariable) {
3143
+ libraryGlobalVars.add(lib.globalVariable);
3144
+ }
3145
+ }
3146
+ }
3147
+ // Check for invalid destructuring from components
1632
3148
  (0, traverse_1.default)(ast, {
1633
- CallExpression(path) {
1634
- // Check for onSaveUserSettings calls
1635
- if (t.isOptionalCallExpression(path.node) || t.isCallExpression(path.node)) {
1636
- const callee = path.node.callee;
1637
- if (t.isIdentifier(callee) && callee.name === 'onSaveUserSettings') {
1638
- // Check if this is inside an onChange/onInput handler
1639
- let parent = path.getFunctionParent();
1640
- if (parent) {
1641
- const funcName = ComponentLinter.getFunctionName(parent);
1642
- if (funcName && (funcName.includes('Change') || funcName.includes('Input'))) {
1643
- // Check if it's not debounced or on blur
1644
- const parentBody = parent.node.body;
1645
- const hasDebounce = parentBody && parentBody.toString().includes('debounce');
1646
- const hasTimeout = parentBody && parentBody.toString().includes('setTimeout');
1647
- if (!hasDebounce && !hasTimeout) {
3149
+ VariableDeclarator(path) {
3150
+ // Look for: const { Something } = components;
3151
+ if (t.isObjectPattern(path.node.id) &&
3152
+ t.isIdentifier(path.node.init) &&
3153
+ path.node.init.name === 'components') {
3154
+ for (const prop of path.node.id.properties) {
3155
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3156
+ const destructuredName = prop.key.name;
3157
+ // Check if this is NOT a valid component from dependencies
3158
+ if (!validComponentNames.has(destructuredName)) {
3159
+ // Check if it might be a library being incorrectly destructured
3160
+ if (libraryNames.has(destructuredName) || libraryGlobalVars.has(destructuredName)) {
1648
3161
  violations.push({
1649
- rule: 'noisy-settings-updates',
3162
+ rule: 'invalid-components-destructuring',
1650
3163
  severity: 'critical',
1651
- line: path.node.loc?.start.line || 0,
1652
- column: path.node.loc?.start.column || 0,
1653
- message: `Saving settings on every change/keystroke. Save on blur, submit, or after debouncing.`
3164
+ line: prop.loc?.start.line || 0,
3165
+ column: prop.loc?.start.column || 0,
3166
+ message: `Attempting to destructure library "${destructuredName}" from components prop. Libraries should be accessed directly via their globalVariable, not from components.`,
3167
+ code: `const { ${destructuredName} } = components;`
3168
+ });
3169
+ }
3170
+ else {
3171
+ violations.push({
3172
+ rule: 'invalid-components-destructuring',
3173
+ severity: 'high',
3174
+ line: prop.loc?.start.line || 0,
3175
+ column: prop.loc?.start.column || 0,
3176
+ message: `Destructuring "${destructuredName}" from components prop, but it's not in the component's dependencies array. Either add it to dependencies or it might be a missing library.`,
3177
+ code: `const { ${destructuredName} } = components;`
1654
3178
  });
1655
3179
  }
1656
3180
  }
@@ -1663,109 +3187,380 @@ ComponentLinter.universalComponentRules = [
1663
3187
  }
1664
3188
  },
1665
3189
  {
1666
- name: 'prop-state-sync',
3190
+ name: 'unsafe-array-operations',
1667
3191
  appliesTo: 'all',
1668
3192
  test: (ast, componentName, componentSpec) => {
1669
3193
  const violations = [];
3194
+ // Track which parameters are from props (likely from queries/RunView)
3195
+ const propsParams = new Set();
1670
3196
  (0, traverse_1.default)(ast, {
1671
- CallExpression(path) {
1672
- if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useEffect') {
1673
- const effectBody = path.node.arguments[0];
1674
- const deps = path.node.arguments[1];
1675
- if (effectBody && (t.isArrowFunctionExpression(effectBody) || t.isFunctionExpression(effectBody))) {
1676
- const bodyString = effectBody.body.toString();
1677
- // Check if it's setting state based on props
1678
- const hasSetState = /set[A-Z]\w*\s*\(/.test(bodyString);
1679
- const depsString = deps ? deps.toString() : '';
1680
- // Check if deps include prop-like names
1681
- const propPatterns = ['Prop', 'value', 'data', 'items'];
1682
- const hasPropDeps = propPatterns.some(p => depsString.includes(p));
1683
- if (hasSetState && hasPropDeps && !bodyString.includes('async')) {
1684
- violations.push({
1685
- rule: 'prop-state-sync',
1686
- severity: 'critical',
1687
- line: path.node.loc?.start.line || 0,
1688
- column: path.node.loc?.start.column || 0,
1689
- message: 'Syncing props to internal state with useEffect creates dual state management',
1690
- code: path.toString().substring(0, 100)
3197
+ // Find the main component function to identify props
3198
+ FunctionDeclaration(path) {
3199
+ if (path.node.id?.name === componentName) {
3200
+ const params = path.node.params[0];
3201
+ if (t.isObjectPattern(params)) {
3202
+ params.properties.forEach(prop => {
3203
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3204
+ propsParams.add(prop.key.name);
3205
+ }
3206
+ });
3207
+ }
3208
+ }
3209
+ },
3210
+ FunctionExpression(path) {
3211
+ // Also check function expressions
3212
+ const parent = path.parent;
3213
+ if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
3214
+ if (parent.id.name === componentName) {
3215
+ const params = path.node.params[0];
3216
+ if (t.isObjectPattern(params)) {
3217
+ params.properties.forEach(prop => {
3218
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3219
+ propsParams.add(prop.key.name);
3220
+ }
1691
3221
  });
1692
3222
  }
1693
3223
  }
1694
3224
  }
3225
+ },
3226
+ // Check for unsafe array operations
3227
+ MemberExpression(path) {
3228
+ const { object, property } = path.node;
3229
+ // Check for array methods that will crash on undefined
3230
+ const unsafeArrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'length'];
3231
+ if (t.isIdentifier(property) && unsafeArrayMethods.includes(property.name)) {
3232
+ // Check if the object is a prop parameter
3233
+ if (t.isIdentifier(object) && propsParams.has(object.name)) {
3234
+ // Look for common data prop patterns
3235
+ const isDataProp = object.name.toLowerCase().includes('data') ||
3236
+ object.name.toLowerCase().includes('items') ||
3237
+ object.name.toLowerCase().includes('results') ||
3238
+ object.name.toLowerCase().includes('records') ||
3239
+ object.name.toLowerCase().includes('list') ||
3240
+ object.name.toLowerCase().includes('types') ||
3241
+ object.name.toLowerCase().includes('options');
3242
+ if (isDataProp || property.name === 'length') {
3243
+ // Check if there's a guard nearby (within the same function/block)
3244
+ let hasGuard = false;
3245
+ // Check for optional chaining (?.length, ?.map)
3246
+ if (path.node.optional) {
3247
+ hasGuard = true;
3248
+ }
3249
+ // Check for (data || []).map pattern
3250
+ const parent = path.parent;
3251
+ if (t.isMemberExpression(parent) && t.isLogicalExpression(parent.object)) {
3252
+ if (parent.object.operator === '||' && t.isIdentifier(parent.object.left)) {
3253
+ if (parent.object.left.name === object.name) {
3254
+ hasGuard = true;
3255
+ }
3256
+ }
3257
+ }
3258
+ // Check for inline guards like: data && data.map(...)
3259
+ const grandParent = path.parentPath?.parent;
3260
+ if (t.isLogicalExpression(grandParent) && grandParent.operator === '&&') {
3261
+ if (t.isIdentifier(grandParent.left) && grandParent.left.name === object.name) {
3262
+ hasGuard = true;
3263
+ }
3264
+ }
3265
+ // Check for early return guards in the function
3266
+ // This is harder to detect perfectly, but we can look for common patterns
3267
+ const functionParent = path.getFunctionParent();
3268
+ if (functionParent && !hasGuard) {
3269
+ let hasEarlyReturn = false;
3270
+ // Look for if statements with returns that check our variable
3271
+ functionParent.traverse({
3272
+ IfStatement(ifPath) {
3273
+ // Skip if this if statement comes after our usage
3274
+ if (ifPath.node.loc && path.node.loc) {
3275
+ if (ifPath.node.loc.start.line > path.node.loc.start.line) {
3276
+ return;
3277
+ }
3278
+ }
3279
+ const test = ifPath.node.test;
3280
+ let checksOurVariable = false;
3281
+ // Check if the test involves our variable
3282
+ if (t.isUnaryExpression(test) && test.operator === '!') {
3283
+ if (t.isIdentifier(test.argument) && test.argument.name === object.name) {
3284
+ checksOurVariable = true;
3285
+ }
3286
+ }
3287
+ if (t.isLogicalExpression(test)) {
3288
+ // Check for !data || !Array.isArray(data) pattern
3289
+ ifPath.traverse({
3290
+ Identifier(idPath) {
3291
+ if (idPath.node.name === object.name) {
3292
+ checksOurVariable = true;
3293
+ }
3294
+ }
3295
+ });
3296
+ }
3297
+ // Check if the consequent has a return statement
3298
+ if (checksOurVariable) {
3299
+ const consequent = ifPath.node.consequent;
3300
+ if (t.isBlockStatement(consequent)) {
3301
+ for (const stmt of consequent.body) {
3302
+ if (t.isReturnStatement(stmt)) {
3303
+ hasEarlyReturn = true;
3304
+ break;
3305
+ }
3306
+ }
3307
+ }
3308
+ else if (t.isReturnStatement(consequent)) {
3309
+ hasEarlyReturn = true;
3310
+ }
3311
+ }
3312
+ }
3313
+ });
3314
+ if (hasEarlyReturn) {
3315
+ hasGuard = true;
3316
+ }
3317
+ }
3318
+ if (!hasGuard) {
3319
+ const methodName = property.name;
3320
+ const severity = methodName === 'length' ? 'high' : 'high';
3321
+ violations.push({
3322
+ rule: 'unsafe-array-operations',
3323
+ severity,
3324
+ line: path.node.loc?.start.line || 0,
3325
+ column: path.node.loc?.start.column || 0,
3326
+ message: `Unsafe operation "${object.name}.${methodName}" on prop that may be undefined. Props from queries/RunView can be null/undefined on initial render. Add a guard: if (!${object.name} || !Array.isArray(${object.name})) return <div>Loading...</div>; OR use: ${object.name}?.${methodName} or (${object.name} || []).${methodName}`,
3327
+ code: `${object.name}.${methodName}`
3328
+ });
3329
+ }
3330
+ }
3331
+ }
3332
+ }
1695
3333
  }
1696
3334
  });
1697
3335
  return violations;
1698
3336
  }
1699
3337
  },
1700
3338
  {
1701
- name: 'performance-memoization',
3339
+ name: 'undefined-jsx-component',
1702
3340
  appliesTo: 'all',
1703
3341
  test: (ast, componentName, componentSpec) => {
1704
3342
  const violations = [];
1705
- const memoizedValues = new Set();
1706
- // Collect memoized values
1707
- (0, traverse_1.default)(ast, {
1708
- CallExpression(path) {
1709
- if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useMemo') {
1710
- // Find the variable being assigned
1711
- if (t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) {
1712
- memoizedValues.add(path.parent.id.name);
1713
- }
3343
+ // Track what's available in scope
3344
+ const availableIdentifiers = new Set();
3345
+ const componentsFromProp = new Set();
3346
+ const libraryGlobalVars = new Set();
3347
+ // Add React hooks and built-ins
3348
+ availableIdentifiers.add('React');
3349
+ REACT_BUILT_INS.forEach(name => availableIdentifiers.add(name));
3350
+ availableIdentifiers.add('useState');
3351
+ availableIdentifiers.add('useEffect');
3352
+ availableIdentifiers.add('useCallback');
3353
+ availableIdentifiers.add('useMemo');
3354
+ availableIdentifiers.add('useRef');
3355
+ availableIdentifiers.add('useContext');
3356
+ availableIdentifiers.add('useReducer');
3357
+ availableIdentifiers.add('useLayoutEffect');
3358
+ // Add HTML elements from our comprehensive list
3359
+ HTML_ELEMENTS.forEach(el => availableIdentifiers.add(el));
3360
+ // Add library global variables
3361
+ if (componentSpec?.libraries) {
3362
+ for (const lib of componentSpec.libraries) {
3363
+ if (lib.globalVariable) {
3364
+ libraryGlobalVars.add(lib.globalVariable);
3365
+ availableIdentifiers.add(lib.globalVariable);
1714
3366
  }
1715
3367
  }
1716
- });
1717
- // Check for expensive operations without memoization
3368
+ }
3369
+ // Track what's destructured from components
3370
+ if (componentSpec?.dependencies) {
3371
+ for (const dep of componentSpec.dependencies) {
3372
+ if (dep.name) {
3373
+ componentsFromProp.add(dep.name);
3374
+ }
3375
+ }
3376
+ }
1718
3377
  (0, traverse_1.default)(ast, {
1719
- CallExpression(path) {
1720
- if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
1721
- const method = path.node.callee.property.name;
1722
- // Check for expensive array operations
1723
- if (['filter', 'sort', 'map', 'reduce'].includes(method)) {
1724
- // Check if this is inside a variable declaration
1725
- let parentPath = path.parentPath;
1726
- while (parentPath && !t.isVariableDeclarator(parentPath.node)) {
1727
- parentPath = parentPath.parentPath;
3378
+ // Track variable declarations
3379
+ VariableDeclarator(path) {
3380
+ if (t.isIdentifier(path.node.id)) {
3381
+ availableIdentifiers.add(path.node.id.name);
3382
+ }
3383
+ else if (t.isObjectPattern(path.node.id)) {
3384
+ // Track destructured variables
3385
+ for (const prop of path.node.id.properties) {
3386
+ if (t.isObjectProperty(prop)) {
3387
+ if (t.isIdentifier(prop.value)) {
3388
+ availableIdentifiers.add(prop.value.name);
3389
+ }
3390
+ else if (t.isIdentifier(prop.key)) {
3391
+ availableIdentifiers.add(prop.key.name);
3392
+ }
1728
3393
  }
1729
- if (parentPath && t.isVariableDeclarator(parentPath.node) && t.isIdentifier(parentPath.node.id)) {
1730
- const varName = parentPath.node.id.name;
1731
- // Check if it's not memoized
1732
- if (!memoizedValues.has(varName)) {
1733
- // Check if it's in the render method (not in event handlers)
1734
- let funcParent = path.getFunctionParent();
1735
- if (funcParent) {
1736
- const funcName = ComponentLinter.getFunctionName(funcParent);
1737
- if (!funcName || funcName === componentName) {
1738
- violations.push({
1739
- rule: 'performance-memoization',
1740
- severity: 'low', // Just a suggestion, not mandatory
1741
- line: path.node.loc?.start.line || 0,
1742
- column: path.node.loc?.start.column || 0,
1743
- message: `Expensive ${method} operation without memoization. Consider using useMemo.`,
1744
- code: `const ${varName} = ...${method}(...)`
1745
- });
3394
+ }
3395
+ }
3396
+ },
3397
+ // Track function declarations
3398
+ FunctionDeclaration(path) {
3399
+ if (path.node.id) {
3400
+ availableIdentifiers.add(path.node.id.name);
3401
+ }
3402
+ },
3403
+ // Check JSX elements
3404
+ JSXElement(path) {
3405
+ const openingElement = path.node.openingElement;
3406
+ // Handle JSXMemberExpression (e.g., <library.Component>)
3407
+ if (t.isJSXMemberExpression(openingElement.name)) {
3408
+ let objectName = '';
3409
+ if (t.isJSXIdentifier(openingElement.name.object)) {
3410
+ objectName = openingElement.name.object.name;
3411
+ }
3412
+ // Check if the object (library global) is available
3413
+ if (objectName && !availableIdentifiers.has(objectName)) {
3414
+ // Check if it looks like a library global that should exist
3415
+ const isLikelyLibrary = /^[a-z][a-zA-Z]*$/.test(objectName) || // camelCase like agGrid
3416
+ /^[A-Z][a-zA-Z]*$/.test(objectName); // PascalCase like MaterialUI
3417
+ if (isLikelyLibrary) {
3418
+ // Suggest available library globals
3419
+ const availableLibraries = Array.from(libraryGlobalVars);
3420
+ if (availableLibraries.length > 0) {
3421
+ // Try to find a close match
3422
+ let suggestion = '';
3423
+ for (const lib of availableLibraries) {
3424
+ // Check for common patterns like agGridReact -> agGrid
3425
+ if (objectName.toLowerCase().includes(lib.toLowerCase().replace('grid', '')) ||
3426
+ lib.toLowerCase().includes(objectName.toLowerCase().replace('react', ''))) {
3427
+ suggestion = lib;
3428
+ break;
1746
3429
  }
1747
3430
  }
3431
+ if (suggestion) {
3432
+ violations.push({
3433
+ rule: 'undefined-jsx-component',
3434
+ severity: 'critical',
3435
+ line: openingElement.loc?.start.line || 0,
3436
+ column: openingElement.loc?.start.column || 0,
3437
+ message: `Library global "${objectName}" is not defined. Did you mean "${suggestion}"? Available library globals: ${availableLibraries.join(', ')}`,
3438
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
3439
+ });
3440
+ }
3441
+ else {
3442
+ violations.push({
3443
+ rule: 'undefined-jsx-component',
3444
+ severity: 'critical',
3445
+ line: openingElement.loc?.start.line || 0,
3446
+ column: openingElement.loc?.start.column || 0,
3447
+ message: `Library global "${objectName}" is not defined. Available library globals: ${availableLibraries.join(', ')}`,
3448
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
3449
+ });
3450
+ }
3451
+ }
3452
+ else {
3453
+ violations.push({
3454
+ rule: 'undefined-jsx-component',
3455
+ severity: 'critical',
3456
+ line: openingElement.loc?.start.line || 0,
3457
+ column: openingElement.loc?.start.column || 0,
3458
+ message: `"${objectName}" is not defined. It appears to be a library global, but no libraries are specified in the component specification.`,
3459
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
3460
+ });
1748
3461
  }
1749
3462
  }
3463
+ else {
3464
+ // Not a typical library pattern, just undefined
3465
+ violations.push({
3466
+ rule: 'undefined-jsx-component',
3467
+ severity: 'critical',
3468
+ line: openingElement.loc?.start.line || 0,
3469
+ column: openingElement.loc?.start.column || 0,
3470
+ message: `"${objectName}" is not defined in the current scope.`,
3471
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
3472
+ });
3473
+ }
1750
3474
  }
3475
+ return; // Done with member expression
1751
3476
  }
1752
- },
1753
- // Check for static arrays/objects
1754
- VariableDeclarator(path) {
1755
- if (t.isIdentifier(path.node.id) &&
1756
- (t.isArrayExpression(path.node.init) || t.isObjectExpression(path.node.init))) {
1757
- const varName = path.node.id.name;
1758
- if (!memoizedValues.has(varName)) {
1759
- // Check if it looks static (no variables referenced)
1760
- const hasVariables = path.node.init.toString().match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g);
1761
- if (!hasVariables || hasVariables.length < 3) { // Allow some property names
3477
+ // Handle regular JSXIdentifier (e.g., <Component>)
3478
+ if (t.isJSXIdentifier(openingElement.name)) {
3479
+ const tagName = openingElement.name.name;
3480
+ // Check if this component is available in scope
3481
+ if (!availableIdentifiers.has(tagName)) {
3482
+ // It's not defined - check if it's a built-in or needs to be defined
3483
+ const isHTMLElement = HTML_ELEMENTS.has(tagName.toLowerCase());
3484
+ const isReactBuiltIn = REACT_BUILT_INS.has(tagName);
3485
+ if (!isHTMLElement && !isReactBuiltIn) {
3486
+ // Not a built-in element, so it needs to be defined
3487
+ // Check if it looks like PascalCase (likely a component)
3488
+ const isPascalCase = /^[A-Z][a-zA-Z0-9]*$/.test(tagName);
3489
+ if (isPascalCase) {
3490
+ // Check what libraries are actually available in the spec
3491
+ const availableLibraries = componentSpec?.libraries || [];
3492
+ if (availableLibraries.length > 0) {
3493
+ // We have libraries available - provide specific guidance
3494
+ const libraryNames = availableLibraries
3495
+ .filter(lib => lib.globalVariable)
3496
+ .map(lib => lib.globalVariable);
3497
+ if (libraryNames.length === 1) {
3498
+ // Single library - be very specific
3499
+ violations.push({
3500
+ rule: 'undefined-jsx-component',
3501
+ severity: 'critical',
3502
+ line: openingElement.loc?.start.line || 0,
3503
+ column: openingElement.loc?.start.column || 0,
3504
+ message: `JSX component "${tagName}" is not defined. This looks like it should be destructured from the ${libraryNames[0]} library. Add: const { ${tagName} } = ${libraryNames[0]}; at the top of your component function.`,
3505
+ code: `<${tagName} ... />`
3506
+ });
3507
+ }
3508
+ else {
3509
+ // Multiple libraries - suggest checking which one
3510
+ violations.push({
3511
+ rule: 'undefined-jsx-component',
3512
+ severity: 'critical',
3513
+ line: openingElement.loc?.start.line || 0,
3514
+ column: openingElement.loc?.start.column || 0,
3515
+ message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}. Destructure it from the appropriate library, e.g., const { ${tagName} } = LibraryName;`,
3516
+ code: `<${tagName} ... />`
3517
+ });
3518
+ }
3519
+ }
3520
+ else {
3521
+ // No libraries in spec but looks like a library component
3522
+ violations.push({
3523
+ rule: 'undefined-jsx-component',
3524
+ severity: 'critical',
3525
+ line: openingElement.loc?.start.line || 0,
3526
+ column: openingElement.loc?.start.column || 0,
3527
+ message: `JSX component "${tagName}" is not defined. This appears to be a library component, but no libraries have been specified in the component specification. The use of external libraries has not been authorized for this component. Components without library specifications cannot use external libraries.`,
3528
+ code: `<${tagName} ... />`
3529
+ });
3530
+ }
3531
+ }
3532
+ else if (componentsFromProp.has(tagName)) {
3533
+ // It's a component from the components prop
3534
+ violations.push({
3535
+ rule: 'undefined-jsx-component',
3536
+ severity: 'high',
3537
+ line: openingElement.loc?.start.line || 0,
3538
+ column: openingElement.loc?.start.column || 0,
3539
+ message: `JSX component "${tagName}" is in dependencies but not destructured from components prop. Add: const { ${tagName} } = components;`,
3540
+ code: `<${tagName} ... />`
3541
+ });
3542
+ }
3543
+ else {
3544
+ // Unknown component - not in libraries, not in dependencies
3545
+ violations.push({
3546
+ rule: 'undefined-jsx-component',
3547
+ severity: 'high',
3548
+ line: openingElement.loc?.start.line || 0,
3549
+ column: openingElement.loc?.start.column || 0,
3550
+ message: `JSX component "${tagName}" is not defined. Either define it in your component, add it to dependencies, or check if it should be destructured from a library.`,
3551
+ code: `<${tagName} ... />`
3552
+ });
3553
+ }
3554
+ }
3555
+ else {
3556
+ // Not PascalCase but also not a built-in - suspicious
1762
3557
  violations.push({
1763
- rule: 'performance-memoization',
1764
- severity: 'low', // Just a suggestion
1765
- line: path.node.loc?.start.line || 0,
1766
- column: path.node.loc?.start.column || 0,
1767
- message: 'Static array/object recreated on every render. Consider using useMemo.',
1768
- code: `const ${varName} = ${path.node.init.type === 'ArrayExpression' ? '[...]' : '{...}'}`
3558
+ rule: 'undefined-jsx-component',
3559
+ severity: 'medium',
3560
+ line: openingElement.loc?.start.line || 0,
3561
+ column: openingElement.loc?.start.column || 0,
3562
+ message: `JSX element "${tagName}" is not recognized as a valid HTML element or React component. Check the spelling or ensure it's properly defined.`,
3563
+ code: `<${tagName} ... />`
1769
3564
  });
1770
3565
  }
1771
3566
  }
@@ -1776,34 +3571,42 @@ ComponentLinter.universalComponentRules = [
1776
3571
  }
1777
3572
  },
1778
3573
  {
1779
- name: 'child-state-management',
3574
+ name: 'runview-runquery-result-direct-usage',
1780
3575
  appliesTo: 'all',
1781
3576
  test: (ast, componentName, componentSpec) => {
1782
3577
  const violations = [];
3578
+ // Track variables that hold RunView/RunQuery results
3579
+ const resultVariables = new Map();
1783
3580
  (0, traverse_1.default)(ast, {
1784
- CallExpression(path) {
1785
- if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useState') {
1786
- // Check if the state name suggests child component state
1787
- if (t.isVariableDeclarator(path.parent) && t.isArrayPattern(path.parent.id)) {
1788
- const stateNameNode = path.parent.id.elements[0];
1789
- if (t.isIdentifier(stateNameNode)) {
1790
- const stateName = stateNameNode.name;
1791
- // Check for patterns suggesting child state management
1792
- const childPatterns = [
1793
- /^child/i,
1794
- /Table\w*State/,
1795
- /Panel\w*State/,
1796
- /Modal\w*State/,
1797
- /\w+Component\w*/
1798
- ];
1799
- if (childPatterns.some(pattern => pattern.test(stateName))) {
1800
- violations.push({
1801
- rule: 'child-state-management',
1802
- severity: 'critical',
1803
- line: path.node.loc?.start.line || 0,
1804
- column: path.node.loc?.start.column || 0,
1805
- message: `Component trying to manage child component state: ${stateName}. Child components manage their own state!`,
1806
- code: `const [${stateName}, ...] = useState(...)`
3581
+ // First pass: identify RunView/RunQuery calls and their assigned variables
3582
+ AwaitExpression(path) {
3583
+ const callExpr = path.node.argument;
3584
+ if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
3585
+ const callee = callExpr.callee;
3586
+ // Check for utilities.rv.RunView or utilities.rq.RunQuery pattern
3587
+ if (t.isMemberExpression(callee.object) &&
3588
+ t.isIdentifier(callee.object.object) &&
3589
+ callee.object.object.name === 'utilities') {
3590
+ const method = t.isIdentifier(callee.property) ? callee.property.name : '';
3591
+ const isRunView = method === 'RunView' || method === 'RunViews';
3592
+ const isRunQuery = method === 'RunQuery';
3593
+ if (isRunView || isRunQuery) {
3594
+ // Check if this is being assigned to a variable
3595
+ const parent = path.parent;
3596
+ if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
3597
+ // const result = await utilities.rv.RunView(...)
3598
+ resultVariables.set(parent.id.name, {
3599
+ line: parent.id.loc?.start.line || 0,
3600
+ column: parent.id.loc?.start.column || 0,
3601
+ method: isRunView ? 'RunView' : 'RunQuery'
3602
+ });
3603
+ }
3604
+ else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
3605
+ // result = await utilities.rv.RunView(...)
3606
+ resultVariables.set(parent.left.name, {
3607
+ line: parent.left.loc?.start.line || 0,
3608
+ column: parent.left.loc?.start.column || 0,
3609
+ method: isRunView ? 'RunView' : 'RunQuery'
1807
3610
  });
1808
3611
  }
1809
3612
  }
@@ -1811,34 +3614,84 @@ ComponentLinter.universalComponentRules = [
1811
3614
  }
1812
3615
  }
1813
3616
  });
1814
- return violations;
1815
- }
1816
- },
1817
- {
1818
- name: 'server-reload-on-client-operation',
1819
- appliesTo: 'all',
1820
- test: (ast, componentName, componentSpec) => {
1821
- const violations = [];
3617
+ // Second pass: check for misuse of these result variables
1822
3618
  (0, traverse_1.default)(ast, {
3619
+ // Check for direct array operations
1823
3620
  CallExpression(path) {
1824
- const callee = path.node.callee;
1825
- // Look for data loading functions
1826
- if (t.isIdentifier(callee) &&
1827
- (callee.name.includes('load') || callee.name.includes('fetch'))) {
1828
- // Check if it's called in sort/filter handlers
1829
- let funcParent = path.getFunctionParent();
1830
- if (funcParent) {
1831
- const funcName = ComponentLinter.getFunctionName(funcParent);
1832
- if (funcName &&
1833
- (funcName.includes('Sort') || funcName.includes('Filter') ||
1834
- funcName.includes('handleSort') || funcName.includes('handleFilter'))) {
3621
+ // Check for array methods being called on result objects
3622
+ if (t.isMemberExpression(path.node.callee) &&
3623
+ t.isIdentifier(path.node.callee.object) &&
3624
+ t.isIdentifier(path.node.callee.property)) {
3625
+ const objName = path.node.callee.object.name;
3626
+ const methodName = path.node.callee.property.name;
3627
+ // Array methods that would fail on a result object
3628
+ const arrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'sort', 'concat'];
3629
+ if (resultVariables.has(objName) && arrayMethods.includes(methodName)) {
3630
+ const resultInfo = resultVariables.get(objName);
3631
+ violations.push({
3632
+ rule: 'runview-runquery-result-direct-usage',
3633
+ severity: 'critical',
3634
+ line: path.node.loc?.start.line || 0,
3635
+ column: path.node.loc?.start.column || 0,
3636
+ message: `Cannot call array method "${methodName}" directly on ${resultInfo.method} result. Use "${objName}.Results.${methodName}(...)" instead. ${resultInfo.method} returns an object with { Success, Results, ... }, not an array.`,
3637
+ code: `${objName}.${methodName}(...)`
3638
+ });
3639
+ }
3640
+ }
3641
+ },
3642
+ // Check for direct usage in setState or as function arguments
3643
+ Identifier(path) {
3644
+ const varName = path.node.name;
3645
+ if (resultVariables.has(varName)) {
3646
+ const resultInfo = resultVariables.get(varName);
3647
+ const parent = path.parent;
3648
+ // Check if being passed to setState-like functions
3649
+ if (t.isCallExpression(parent) && path.node === parent.arguments[0]) {
3650
+ const callee = parent.callee;
3651
+ // Check for setState patterns
3652
+ if (t.isIdentifier(callee) && /^set[A-Z]/.test(callee.name)) {
3653
+ // Likely a setState function
1835
3654
  violations.push({
1836
- rule: 'server-reload-on-client-operation',
3655
+ rule: 'runview-runquery-result-direct-usage',
1837
3656
  severity: 'critical',
1838
3657
  line: path.node.loc?.start.line || 0,
1839
3658
  column: path.node.loc?.start.column || 0,
1840
- message: 'Reloading data from server on sort/filter. Use useMemo for client-side operations.',
1841
- code: `${funcName} calls ${callee.name}`
3659
+ message: `Passing ${resultInfo.method} result directly to setState. Use "${varName}.Results" or check "${varName}.Success" first. ${resultInfo.method} returns { Success, Results, ErrorMessage }, not the data array.`,
3660
+ code: `${callee.name}(${varName})`
3661
+ });
3662
+ }
3663
+ // Check for array-expecting functions
3664
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
3665
+ const methodName = callee.property.name;
3666
+ if (methodName === 'concat' || methodName === 'push' || methodName === 'unshift') {
3667
+ violations.push({
3668
+ rule: 'runview-runquery-result-direct-usage',
3669
+ severity: 'critical',
3670
+ line: path.node.loc?.start.line || 0,
3671
+ column: path.node.loc?.start.column || 0,
3672
+ message: `Passing ${resultInfo.method} result to array method. Use "${varName}.Results" instead of "${varName}".`,
3673
+ code: `...${methodName}(${varName})`
3674
+ });
3675
+ }
3676
+ }
3677
+ }
3678
+ // Check for ternary with Array.isArray check (common pattern)
3679
+ if (t.isConditionalExpression(parent) &&
3680
+ t.isCallExpression(parent.test) &&
3681
+ t.isMemberExpression(parent.test.callee) &&
3682
+ t.isIdentifier(parent.test.callee.object) &&
3683
+ parent.test.callee.object.name === 'Array' &&
3684
+ t.isIdentifier(parent.test.callee.property) &&
3685
+ parent.test.callee.property.name === 'isArray') {
3686
+ // Pattern: Array.isArray(result) ? result : []
3687
+ if (parent.test.arguments[0] === path.node && parent.consequent === path.node) {
3688
+ violations.push({
3689
+ rule: 'runview-runquery-result-direct-usage',
3690
+ severity: 'high',
3691
+ line: path.node.loc?.start.line || 0,
3692
+ column: path.node.loc?.start.column || 0,
3693
+ message: `${resultInfo.method} result is never an array. Use "${varName}.Results || []" instead of "Array.isArray(${varName}) ? ${varName} : []".`,
3694
+ code: `Array.isArray(${varName}) ? ${varName} : []`
1842
3695
  });
1843
3696
  }
1844
3697
  }
@@ -1849,194 +3702,298 @@ ComponentLinter.universalComponentRules = [
1849
3702
  }
1850
3703
  },
1851
3704
  {
1852
- name: 'runview-runquery-valid-properties',
3705
+ name: 'runquery-runview-result-structure',
1853
3706
  appliesTo: 'all',
1854
3707
  test: (ast, componentName, componentSpec) => {
1855
3708
  const violations = [];
1856
- // Valid properties for RunView/RunViews
1857
- const validRunViewProps = new Set([
1858
- 'ViewID', 'ViewName', 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
1859
- 'MaxRows', 'StartRow', 'ResultType', 'UserSearchString', 'ForceAuditLog', 'AuditLogDescription',
1860
- 'ResultType'
3709
+ // Valid properties for RunQueryResult
3710
+ const validRunQueryResultProps = new Set([
3711
+ 'QueryID', 'QueryName', 'Success', 'Results', 'RowCount',
3712
+ 'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
1861
3713
  ]);
1862
- // Valid properties for RunQuery
1863
- const validRunQueryProps = new Set([
1864
- 'QueryID', 'QueryName', 'CategoryID', 'CategoryPath', 'Parameters', 'MaxRows', 'StartRow', 'ForceAuditLog', 'AuditLogDescription'
3714
+ // Valid properties for RunViewResult
3715
+ const validRunViewResultProps = new Set([
3716
+ 'Success', 'Results', 'UserViewRunID', 'RowCount',
3717
+ 'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
1865
3718
  ]);
3719
+ // Common incorrect patterns
3720
+ const invalidResultPatterns = new Set(['data', 'rows', 'records', 'items', 'values']);
1866
3721
  (0, traverse_1.default)(ast, {
1867
- CallExpression(path) {
1868
- const callee = path.node.callee;
1869
- // Check for utilities.rv.RunView or utilities.rv.RunViews
1870
- if (t.isMemberExpression(callee) &&
1871
- t.isMemberExpression(callee.object) &&
1872
- t.isIdentifier(callee.object.object) &&
1873
- callee.object.object.name === 'utilities' &&
1874
- t.isIdentifier(callee.object.property) &&
1875
- callee.object.property.name === 'rv' &&
1876
- t.isIdentifier(callee.property)) {
1877
- const methodName = callee.property.name;
1878
- if (methodName === 'RunView' || methodName === 'RunViews') {
1879
- // Get the config object(s)
1880
- let configs = [];
1881
- if (methodName === 'RunViews' && path.node.arguments[0]) {
1882
- // RunViews takes an array of configs
1883
- if (t.isArrayExpression(path.node.arguments[0])) {
1884
- configs = path.node.arguments[0].elements
1885
- .filter((e) => t.isObjectExpression(e));
1886
- }
3722
+ MemberExpression(path) {
3723
+ // Check if this is accessing a property on a variable that looks like a query/view result
3724
+ if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
3725
+ const objName = path.node.object.name;
3726
+ const propName = path.node.property.name;
3727
+ // Check if the object name suggests it's a query or view result
3728
+ const isLikelyQueryResult = /result|response|res|data|output/i.test(objName);
3729
+ const isFromRunQuery = path.scope.hasBinding(objName) &&
3730
+ ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
3731
+ const isFromRunView = path.scope.hasBinding(objName) &&
3732
+ ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
3733
+ if (isLikelyQueryResult || isFromRunQuery || isFromRunView) {
3734
+ // Check for common incorrect patterns
3735
+ if (invalidResultPatterns.has(propName)) {
3736
+ violations.push({
3737
+ rule: 'runquery-runview-result-structure',
3738
+ severity: 'high',
3739
+ line: path.node.loc?.start.line || 0,
3740
+ column: path.node.loc?.start.column || 0,
3741
+ message: `Incorrect property access "${objName}.${propName}". RunQuery/RunView results use ".Results" for data array, not ".${propName}". Change to "${objName}.Results"`,
3742
+ code: `${objName}.${propName}`
3743
+ });
1887
3744
  }
1888
- else if (methodName === 'RunView' && path.node.arguments[0]) {
1889
- // RunView takes a single config
1890
- if (t.isObjectExpression(path.node.arguments[0])) {
1891
- configs = [path.node.arguments[0]];
1892
- }
3745
+ else if (propName === 'data') {
3746
+ // Special case for .data - very common mistake
3747
+ violations.push({
3748
+ rule: 'runquery-runview-result-structure',
3749
+ severity: 'critical',
3750
+ line: path.node.loc?.start.line || 0,
3751
+ column: path.node.loc?.start.column || 0,
3752
+ message: `RunQuery/RunView results don't have a ".data" property. Use ".Results" to access the array of returned rows. Change "${objName}.data" to "${objName}.Results"`,
3753
+ code: `${objName}.${propName}`
3754
+ });
1893
3755
  }
1894
- // Check each config for invalid properties
1895
- for (const config of configs) {
1896
- for (const prop of config.properties) {
1897
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1898
- const propName = prop.key.name;
1899
- if (!validRunViewProps.has(propName)) {
1900
- // Special error messages for common mistakes
1901
- let message = `Invalid property '${propName}' on ${methodName}. Valid properties: ${Array.from(validRunViewProps).join(', ')}`;
1902
- let fix = `Remove '${propName}' property`;
1903
- if (propName === 'Parameters') {
1904
- message = `${methodName} does not support 'Parameters'. Use 'ExtraFilter' for WHERE clauses.`;
1905
- fix = `Replace 'Parameters' with 'ExtraFilter' and format as SQL WHERE clause`;
1906
- }
1907
- else if (propName === 'GroupBy') {
1908
- message = `${methodName} does not support 'GroupBy'. Use RunQuery with a pre-defined query for aggregations.`;
1909
- fix = `Remove 'GroupBy' and use RunQuery instead for aggregated data`;
1910
- }
1911
- else if (propName === 'Having') {
1912
- message = `${methodName} does not support 'Having'. Use RunQuery with a pre-defined query.`;
1913
- fix = `Remove 'Having' and use RunQuery instead`;
1914
- }
1915
- violations.push({
1916
- rule: 'runview-runquery-valid-properties',
1917
- severity: 'critical',
1918
- line: prop.loc?.start.line || 0,
1919
- column: prop.loc?.start.column || 0,
1920
- message,
1921
- code: `${propName}: ...`
1922
- });
1923
- }
1924
- }
1925
- }
3756
+ // Check for nested incorrect access like result.data.entities
3757
+ if (t.isMemberExpression(path.parent) &&
3758
+ t.isIdentifier(path.parent.property) &&
3759
+ propName === 'data') {
3760
+ const nestedProp = path.parent.property.name;
3761
+ violations.push({
3762
+ rule: 'runquery-runview-result-structure',
3763
+ severity: 'critical',
3764
+ line: path.parent.loc?.start.line || 0,
3765
+ column: path.parent.loc?.start.column || 0,
3766
+ message: `Incorrect nested property access "${objName}.data.${nestedProp}". RunQuery/RunView results use ".Results" directly for the data array. Change to "${objName}.Results"`,
3767
+ code: `${objName}.data.${nestedProp}`
3768
+ });
1926
3769
  }
1927
3770
  }
1928
3771
  }
1929
- // Check for utilities.rq.RunQuery
1930
- if (t.isMemberExpression(callee) &&
1931
- t.isMemberExpression(callee.object) &&
1932
- t.isIdentifier(callee.object.object) &&
1933
- callee.object.object.name === 'utilities' &&
1934
- t.isIdentifier(callee.object.property) &&
1935
- callee.object.property.name === 'rq' &&
1936
- t.isIdentifier(callee.property) &&
1937
- callee.property.name === 'RunQuery') {
1938
- if (path.node.arguments[0] && t.isObjectExpression(path.node.arguments[0])) {
1939
- const config = path.node.arguments[0];
1940
- for (const prop of config.properties) {
3772
+ },
3773
+ // Check for destructuring patterns
3774
+ VariableDeclarator(path) {
3775
+ // Check for destructuring from a result object
3776
+ if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
3777
+ const sourceName = path.node.init.name;
3778
+ // Check if this looks like destructuring from a query/view result
3779
+ if (/result|response|res/i.test(sourceName)) {
3780
+ for (const prop of path.node.id.properties) {
1941
3781
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1942
3782
  const propName = prop.key.name;
1943
- if (!validRunQueryProps.has(propName)) {
1944
- let message = `Invalid property '${propName}' on RunQuery. Valid properties: ${Array.from(validRunQueryProps).join(', ')}`;
1945
- let fix = `Remove '${propName}' property`;
1946
- if (propName === 'ExtraFilter') {
1947
- message = `RunQuery does not support 'ExtraFilter'. WHERE clauses should be in the pre-defined query or passed as Parameters.`;
1948
- fix = `Remove 'ExtraFilter'. Add WHERE logic to the query definition or pass as Parameters`;
1949
- }
1950
- else if (propName === 'Fields') {
1951
- message = `RunQuery does not support 'Fields'. The query definition determines returned fields.`;
1952
- fix = `Remove 'Fields'. Modify the query definition to return desired fields`;
1953
- }
1954
- else if (propName === 'OrderBy') {
1955
- message = `RunQuery does not support 'OrderBy'. ORDER BY should be in the query definition.`;
1956
- fix = `Remove 'OrderBy'. Add ORDER BY to the query definition`;
1957
- }
3783
+ // Check for incorrect destructuring
3784
+ if (propName === 'data') {
1958
3785
  violations.push({
1959
- rule: 'runview-runquery-valid-properties',
3786
+ rule: 'runquery-runview-result-structure',
1960
3787
  severity: 'critical',
1961
3788
  line: prop.loc?.start.line || 0,
1962
3789
  column: prop.loc?.start.column || 0,
1963
- message,
1964
- code: `${propName}: ...`
3790
+ message: `Destructuring "data" from RunQuery/RunView result. The property is named "Results", not "data". Change "const { data } = ${sourceName}" to "const { Results } = ${sourceName}"`,
3791
+ code: `{ data }`
3792
+ });
3793
+ }
3794
+ else if (invalidResultPatterns.has(propName) && propName !== 'data') {
3795
+ violations.push({
3796
+ rule: 'runquery-runview-result-structure',
3797
+ severity: 'medium',
3798
+ line: prop.loc?.start.line || 0,
3799
+ column: prop.loc?.start.column || 0,
3800
+ message: `Destructuring "${propName}" from what appears to be a RunQuery/RunView result. Did you mean "Results"?`,
3801
+ code: `{ ${propName} }`
1965
3802
  });
1966
3803
  }
1967
3804
  }
1968
3805
  }
1969
3806
  }
1970
3807
  }
3808
+ },
3809
+ // Check for conditional access without checking Success
3810
+ IfStatement(path) {
3811
+ const test = path.node.test;
3812
+ // Look for patterns like: if (result) or if (result.Results) without checking Success
3813
+ if (t.isIdentifier(test) ||
3814
+ (t.isMemberExpression(test) &&
3815
+ t.isIdentifier(test.object) &&
3816
+ t.isIdentifier(test.property) &&
3817
+ test.property.name === 'Results')) {
3818
+ let varName = '';
3819
+ if (t.isIdentifier(test)) {
3820
+ varName = test.name;
3821
+ }
3822
+ else if (t.isMemberExpression(test) && t.isIdentifier(test.object)) {
3823
+ varName = test.object.name;
3824
+ }
3825
+ // Check if this variable is from RunQuery/RunView
3826
+ if (/result|response|res/i.test(varName)) {
3827
+ // Look for .Results access in the consequent without .Success check
3828
+ let hasResultsAccess = false;
3829
+ let hasSuccessCheck = false;
3830
+ (0, traverse_1.default)(path.node, {
3831
+ MemberExpression(innerPath) {
3832
+ if (t.isIdentifier(innerPath.node.object) &&
3833
+ innerPath.node.object.name === varName) {
3834
+ if (t.isIdentifier(innerPath.node.property)) {
3835
+ if (innerPath.node.property.name === 'Results') {
3836
+ hasResultsAccess = true;
3837
+ }
3838
+ if (innerPath.node.property.name === 'Success') {
3839
+ hasSuccessCheck = true;
3840
+ }
3841
+ }
3842
+ }
3843
+ }
3844
+ }, path.scope);
3845
+ if (hasResultsAccess && !hasSuccessCheck) {
3846
+ violations.push({
3847
+ rule: 'runquery-runview-result-structure',
3848
+ severity: 'medium',
3849
+ line: test.loc?.start.line || 0,
3850
+ column: test.loc?.start.column || 0,
3851
+ message: `Accessing "${varName}.Results" without checking "${varName}.Success" first. Always verify Success before accessing Results.`,
3852
+ code: `if (${varName}) { ... ${varName}.Results ... }`
3853
+ });
3854
+ }
3855
+ }
3856
+ }
1971
3857
  }
1972
3858
  });
1973
3859
  return violations;
1974
3860
  }
1975
3861
  },
1976
3862
  {
1977
- name: 'root-component-props-restriction',
1978
- appliesTo: 'root',
3863
+ name: 'dependency-prop-validation',
3864
+ appliesTo: 'all',
1979
3865
  test: (ast, componentName, componentSpec) => {
1980
3866
  const violations = [];
1981
- const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
1982
- // This rule applies when testing root components
1983
- // We can identify this by checking if the component spec indicates it's a root component
1984
- // For now, we'll apply this rule universally and let the caller decide when to use it
3867
+ // Skip if no dependencies
3868
+ if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
3869
+ return violations;
3870
+ }
3871
+ // Build a map of dependency components and their expected props
3872
+ const dependencyPropsMap = new Map();
3873
+ for (const dep of componentSpec.dependencies) {
3874
+ const requiredProps = dep.properties
3875
+ ?.filter(p => p.required)
3876
+ ?.map(p => p.name) || [];
3877
+ const allProps = dep.properties?.map(p => p.name) || [];
3878
+ dependencyPropsMap.set(dep.name, {
3879
+ required: requiredProps,
3880
+ all: allProps,
3881
+ location: dep.location || 'embedded'
3882
+ });
3883
+ }
3884
+ // Helper function to find closest matching prop name
3885
+ function findClosestMatch(target, candidates) {
3886
+ if (candidates.length === 0)
3887
+ return null;
3888
+ // Simple Levenshtein distance implementation
3889
+ function levenshtein(a, b) {
3890
+ const matrix = [];
3891
+ for (let i = 0; i <= b.length; i++) {
3892
+ matrix[i] = [i];
3893
+ }
3894
+ for (let j = 0; j <= a.length; j++) {
3895
+ matrix[0][j] = j;
3896
+ }
3897
+ for (let i = 1; i <= b.length; i++) {
3898
+ for (let j = 1; j <= a.length; j++) {
3899
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
3900
+ matrix[i][j] = matrix[i - 1][j - 1];
3901
+ }
3902
+ else {
3903
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
3904
+ }
3905
+ }
3906
+ }
3907
+ return matrix[b.length][a.length];
3908
+ }
3909
+ // Find the closest match
3910
+ let bestMatch = '';
3911
+ let bestDistance = Infinity;
3912
+ for (const candidate of candidates) {
3913
+ const distance = levenshtein(target.toLowerCase(), candidate.toLowerCase());
3914
+ if (distance < bestDistance && distance <= 3) { // Max distance of 3 for suggestions
3915
+ bestDistance = distance;
3916
+ bestMatch = candidate;
3917
+ }
3918
+ }
3919
+ return bestMatch || null;
3920
+ }
3921
+ // Standard props that are always valid (passed by the runtime)
3922
+ const standardProps = new Set([
3923
+ 'styles', 'utilities', 'components', 'callbacks',
3924
+ 'savedUserSettings', 'onSaveUserSettings'
3925
+ ]);
3926
+ // Track JSX elements and their props
1985
3927
  (0, traverse_1.default)(ast, {
1986
- FunctionDeclaration(path) {
1987
- if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
1988
- const param = path.node.params[0];
1989
- if (t.isObjectPattern(param)) {
1990
- const invalidProps = [];
1991
- const allProps = [];
1992
- for (const prop of param.properties) {
1993
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1994
- const propName = prop.key.name;
1995
- allProps.push(propName);
1996
- if (!standardProps.has(propName)) {
1997
- invalidProps.push(propName);
1998
- }
1999
- }
3928
+ JSXElement(path) {
3929
+ const openingElement = path.node.openingElement;
3930
+ // Get the element name
3931
+ let elementName = '';
3932
+ if (t.isJSXIdentifier(openingElement.name)) {
3933
+ elementName = openingElement.name.name;
3934
+ }
3935
+ else if (t.isJSXMemberExpression(openingElement.name)) {
3936
+ // Handle cases like <MaterialUI.Button>
3937
+ return; // Skip member expressions for now
3938
+ }
3939
+ // Check if this is one of our dependencies
3940
+ if (dependencyPropsMap.has(elementName)) {
3941
+ const { required, all, location } = dependencyPropsMap.get(elementName);
3942
+ // Get passed props
3943
+ const passedProps = new Set();
3944
+ const propLocations = new Map();
3945
+ for (const attr of openingElement.attributes) {
3946
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
3947
+ const propName = attr.name.name;
3948
+ passedProps.add(propName);
3949
+ propLocations.set(propName, {
3950
+ line: attr.loc?.start.line || 0,
3951
+ column: attr.loc?.start.column || 0
3952
+ });
2000
3953
  }
2001
- // Only report if there are non-standard props
2002
- // This allows the rule to be selectively applied to root components
2003
- if (invalidProps.length > 0) {
3954
+ }
3955
+ // Check for missing required props
3956
+ for (const requiredProp of required) {
3957
+ if (!passedProps.has(requiredProp) && !standardProps.has(requiredProp)) {
2004
3958
  violations.push({
2005
- rule: 'root-component-props-restriction',
3959
+ rule: 'dependency-prop-validation',
2006
3960
  severity: 'critical',
2007
- line: path.node.loc?.start.line || 0,
2008
- column: path.node.loc?.start.column || 0,
2009
- message: `Component "${componentName}" accepts non-standard props: ${invalidProps.join(', ')}. Root components can only accept standard props: ${Array.from(standardProps).join(', ')}. Load data internally using utilities.rv.RunView().`
3961
+ line: openingElement.loc?.start.line || 0,
3962
+ column: openingElement.loc?.start.column || 0,
3963
+ message: `Missing required prop '${requiredProp}' for dependency component '${elementName}'`,
3964
+ code: `<${elementName} ... />`
2010
3965
  });
2011
3966
  }
2012
3967
  }
2013
- }
2014
- },
2015
- // Also check arrow function components
2016
- VariableDeclarator(path) {
2017
- if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
2018
- const init = path.node.init;
2019
- if (t.isArrowFunctionExpression(init) && init.params[0]) {
2020
- const param = init.params[0];
2021
- if (t.isObjectPattern(param)) {
2022
- const invalidProps = [];
2023
- const allProps = [];
2024
- for (const prop of param.properties) {
2025
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2026
- const propName = prop.key.name;
2027
- allProps.push(propName);
2028
- if (!standardProps.has(propName)) {
2029
- invalidProps.push(propName);
2030
- }
2031
- }
3968
+ // Check for unknown props (potential typos)
3969
+ for (const passedProp of passedProps) {
3970
+ // Skip standard props and spread operators
3971
+ if (standardProps.has(passedProp) || passedProp === 'key' || passedProp === 'ref') {
3972
+ continue;
3973
+ }
3974
+ if (!all.includes(passedProp)) {
3975
+ // Try to find a close match
3976
+ const suggestion = findClosestMatch(passedProp, all);
3977
+ if (suggestion) {
3978
+ const loc = propLocations.get(passedProp);
3979
+ violations.push({
3980
+ rule: 'dependency-prop-validation',
3981
+ severity: 'high',
3982
+ line: loc?.line || openingElement.loc?.start.line || 0,
3983
+ column: loc?.column || openingElement.loc?.start.column || 0,
3984
+ message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Did you mean '${suggestion}'?`,
3985
+ code: `${passedProp}={...}`
3986
+ });
2032
3987
  }
2033
- if (invalidProps.length > 0) {
3988
+ else {
3989
+ const loc = propLocations.get(passedProp);
2034
3990
  violations.push({
2035
- rule: 'root-component-props-restriction',
2036
- severity: 'critical',
2037
- line: path.node.loc?.start.line || 0,
2038
- column: path.node.loc?.start.column || 0,
2039
- message: `Component "${componentName}" accepts non-standard props: ${invalidProps.join(', ')}. Root components can only accept standard props: ${Array.from(standardProps).join(', ')}. Load data internally using utilities.rv.RunView().`
3991
+ rule: 'dependency-prop-validation',
3992
+ severity: 'medium',
3993
+ line: loc?.line || openingElement.loc?.start.line || 0,
3994
+ column: loc?.column || openingElement.loc?.start.column || 0,
3995
+ message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Expected props: ${all.join(', ')}`,
3996
+ code: `${passedProp}={...}`
2040
3997
  });
2041
3998
  }
2042
3999
  }