@memberjunction/react-test-harness 2.82.0 → 2.84.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.
@@ -64,12 +64,23 @@ class ComponentLinter {
64
64
  const dataViolations = this.validateDataRequirements(ast, componentSpec);
65
65
  violations.push(...dataViolations);
66
66
  }
67
+ // Deduplicate violations - keep only unique rule+message combinations
68
+ const uniqueViolations = this.deduplicateViolations(violations);
69
+ // Count violations by severity
70
+ const criticalCount = uniqueViolations.filter(v => v.severity === 'critical').length;
71
+ const highCount = uniqueViolations.filter(v => v.severity === 'high').length;
72
+ const mediumCount = uniqueViolations.filter(v => v.severity === 'medium').length;
73
+ const lowCount = uniqueViolations.filter(v => v.severity === 'low').length;
67
74
  // Generate fix suggestions
68
- const suggestions = this.generateFixSuggestions(violations);
75
+ const suggestions = this.generateFixSuggestions(uniqueViolations);
69
76
  return {
70
- success: violations.filter(v => v.severity === 'error').length === 0,
71
- violations,
72
- suggestions
77
+ success: criticalCount === 0 && highCount === 0, // Only fail on critical/high
78
+ violations: uniqueViolations,
79
+ suggestions,
80
+ criticalCount,
81
+ highCount,
82
+ mediumCount,
83
+ lowCount
73
84
  };
74
85
  }
75
86
  catch (error) {
@@ -78,7 +89,7 @@ class ComponentLinter {
78
89
  success: false,
79
90
  violations: [{
80
91
  rule: 'parse-error',
81
- severity: 'error',
92
+ severity: 'critical',
82
93
  line: 0,
83
94
  column: 0,
84
95
  message: `Failed to parse component: ${error instanceof Error ? error.message : 'Unknown error'}`
@@ -183,7 +194,7 @@ class ComponentLinter {
183
194
  usedEntity.toLowerCase().includes(e.toLowerCase()));
184
195
  violations.push({
185
196
  rule: 'entity-name-mismatch',
186
- severity: 'error',
197
+ severity: 'critical',
187
198
  line: prop.value.loc?.start.line || 0,
188
199
  column: prop.value.loc?.start.column || 0,
189
200
  message: `Entity "${usedEntity}" not found in dataRequirements. ${possibleMatches.length > 0
@@ -206,7 +217,7 @@ class ComponentLinter {
206
217
  if (/COUNT\s*\(|SUM\s*\(|AVG\s*\(|MAX\s*\(|MIN\s*\(/i.test(fieldName)) {
207
218
  violations.push({
208
219
  rule: 'runview-sql-function',
209
- severity: 'error',
220
+ severity: 'critical',
210
221
  line: fieldElement.loc?.start.line || 0,
211
222
  column: fieldElement.loc?.start.column || 0,
212
223
  message: `RunView does not support SQL aggregations. Use RunQuery for aggregations or fetch raw data and aggregate in JavaScript.`,
@@ -221,7 +232,7 @@ class ComponentLinter {
221
232
  if (!isAllowed) {
222
233
  violations.push({
223
234
  rule: 'field-not-in-requirements',
224
- severity: 'error',
235
+ severity: 'critical',
225
236
  line: fieldElement.loc?.start.line || 0,
226
237
  column: fieldElement.loc?.start.column || 0,
227
238
  message: `Field "${fieldName}" not found in dataRequirements for entity "${usedEntity}". Available fields: ${[...entityFields.displayFields, ...entityFields.filterFields, ...entityFields.sortFields].join(', ')}`,
@@ -241,7 +252,7 @@ class ComponentLinter {
241
252
  if (!entityFields.sortFields.has(orderByField)) {
242
253
  violations.push({
243
254
  rule: 'orderby-field-not-sortable',
244
- severity: 'error',
255
+ severity: 'critical',
245
256
  line: orderByProperty.value.loc?.start.line || 0,
246
257
  column: orderByProperty.value.loc?.start.column || 0,
247
258
  message: `OrderBy field "${orderByField}" not in sortFields for entity "${usedEntity}". Available sort fields: ${[...entityFields.sortFields].join(', ')}`,
@@ -282,7 +293,7 @@ class ComponentLinter {
282
293
  usedQuery.toLowerCase().includes(q.toLowerCase()));
283
294
  violations.push({
284
295
  rule: 'query-name-mismatch',
285
- severity: 'error',
296
+ severity: 'critical',
286
297
  line: prop.value.loc?.start.line || 0,
287
298
  column: prop.value.loc?.start.column || 0,
288
299
  message: `Query "${usedQuery}" not found in dataRequirements. ${possibleMatches.length > 0
@@ -323,6 +334,27 @@ class ComponentLinter {
323
334
  }
324
335
  return null;
325
336
  }
337
+ static deduplicateViolations(violations) {
338
+ const seen = new Set();
339
+ const unique = [];
340
+ for (const violation of violations) {
341
+ // Create a key from the complete violation details (case-insensitive for message)
342
+ const key = `${violation.rule}:${violation.severity}:${violation.line}:${violation.column}:${violation.message.toLowerCase()}`;
343
+ if (!seen.has(key)) {
344
+ seen.add(key);
345
+ unique.push(violation);
346
+ }
347
+ }
348
+ // Sort by severity (critical > high > medium > low) and then by line number
349
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
350
+ unique.sort((a, b) => {
351
+ const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
352
+ if (severityDiff !== 0)
353
+ return severityDiff;
354
+ return a.line - b.line;
355
+ });
356
+ return unique;
357
+ }
326
358
  static generateFixSuggestions(violations) {
327
359
  const suggestions = [];
328
360
  for (const violation of violations) {
@@ -330,36 +362,66 @@ class ComponentLinter {
330
362
  case 'full-state-ownership':
331
363
  suggestions.push({
332
364
  violation: violation.rule,
333
- suggestion: 'Components must manage ALL their own state internally. No state props allowed.',
334
- example: `// ❌ WRONG - Expecting state from props:
335
- function Component({ selectedId, filters, sortBy, onStateChange }) {
336
- // This component expects parent to manage its state - WRONG!
365
+ suggestion: 'Components must manage ALL their own state internally. Use proper naming conventions for initialization.',
366
+ example: `// ❌ WRONG - Controlled state props:
367
+ function PaginationControls({ currentPage, filters, sortBy, onPageChange }) {
368
+ // These props suggest parent controls the state - WRONG!
337
369
  }
338
370
 
339
- // CORRECT - Component owns ALL its state:
340
- function Component({ data, savedUserSettings, onSaveUserSettings }) {
341
- // Component manages ALL its own state internally
342
- const [selectedId, setSelectedId] = useState(
343
- savedUserSettings?.selectedId || null
344
- );
345
- const [filters, setFilters] = useState(
346
- savedUserSettings?.filters || {}
347
- );
348
- const [sortBy, setSortBy] = useState(
349
- savedUserSettings?.sortBy || 'name'
371
+ // WRONG - State props without handlers (still controlled):
372
+ function Component({ selectedId, activeTab }) {
373
+ // Parent is managing this component's state
374
+ }
375
+
376
+ // ✅ CORRECT - Using initialization props:
377
+ function PaginationControls({ initialPage, defaultPageSize, onPageChange, savedUserSettings, onSaveUserSettings }) {
378
+ // Component owns ALL its state, initialized from props
379
+ const [currentPage, setCurrentPage] = useState(
380
+ savedUserSettings?.currentPage || initialPage || 1
350
381
  );
382
+ const [pageSize] = useState(defaultPageSize || 10);
351
383
 
352
- // Handle selection
353
- const handleSelect = (id) => {
354
- setSelectedId(id); // Update internal state
384
+ const handlePageChange = (page) => {
385
+ setCurrentPage(page); // Update internal state
386
+ onPageChange?.(page); // Notify parent if needed
355
387
  onSaveUserSettings?.({
356
388
  ...savedUserSettings,
357
- selectedId: id
389
+ currentPage: page
358
390
  });
359
391
  };
360
-
361
- // Each component is generated separately - it must be self-contained
362
- }`
392
+ }
393
+
394
+ // ✅ CORRECT - Configuration props are allowed:
395
+ function DataTable({
396
+ items, // Data prop - allowed
397
+ pageSize, // Configuration - allowed
398
+ maxItems, // Configuration - allowed
399
+ initialSortBy, // Initialization - allowed
400
+ defaultFilters, // Initialization - allowed
401
+ onSelectionChange, // Event handler - allowed
402
+ savedUserSettings,
403
+ onSaveUserSettings
404
+ }) {
405
+ // Component manages its own state
406
+ const [sortBy, setSortBy] = useState(initialSortBy || 'name');
407
+ const [filters, setFilters] = useState(defaultFilters || {});
408
+ const [selectedItems, setSelectedItems] = useState(
409
+ savedUserSettings?.selectedItems || []
410
+ );
411
+ }
412
+
413
+ // Naming conventions:
414
+ // ✅ ALLOWED props:
415
+ // - initial* (initialPage, initialValue, initialSelection)
416
+ // - default* (defaultTab, defaultSortBy, defaultFilters)
417
+ // - Configuration (pageSize, maxItems, minValue, disabled)
418
+ // - Data props (items, options, data, rows, columns)
419
+ // - Event handlers (onChange, onSelect, onPageChange)
420
+
421
+ // ❌ DISALLOWED props (suggest controlled component):
422
+ // - Direct state names (currentPage, selectedId, activeTab)
423
+ // - State without 'initial'/'default' prefix (sortBy, filters, searchTerm)
424
+ // - Controlled patterns (value + onChange, checked + onChange)`
363
425
  });
364
426
  break;
365
427
  case 'no-use-reducer':
@@ -862,6 +924,40 @@ const sortedData = useMemo(() => {
862
924
  }, [data, sortBy, sortDirection]);`
863
925
  });
864
926
  break;
927
+ case 'runview-runquery-valid-properties':
928
+ suggestions.push({
929
+ violation: violation.rule,
930
+ suggestion: 'Use only valid properties for RunView/RunViews and RunQuery',
931
+ example: `// ❌ WRONG - Invalid properties on RunView:
932
+ await utilities.rv.RunView({
933
+ EntityName: 'MJ: AI Prompt Runs',
934
+ Parameters: { startDate, endDate }, // INVALID!
935
+ GroupBy: 'Status' // INVALID!
936
+ });
937
+
938
+ // ✅ CORRECT - Use ExtraFilter for WHERE clauses:
939
+ await utilities.rv.RunView({
940
+ EntityName: 'MJ: AI Prompt Runs',
941
+ ExtraFilter: \`RunAt >= '\${startDate.toISOString()}' AND RunAt <= '\${endDate.toISOString()}'\`,
942
+ OrderBy: 'RunAt DESC',
943
+ Fields: ['RunAt', 'Status', 'Success']
944
+ });
945
+
946
+ // ✅ For aggregations, use RunQuery with a pre-defined query:
947
+ await utilities.rq.RunQuery({
948
+ QueryName: 'Prompt Run Summary',
949
+ Parameters: { startDate, endDate } // Parameters ARE valid for RunQuery
950
+ });
951
+
952
+ // Valid RunView properties:
953
+ // - EntityName (required)
954
+ // - ExtraFilter, OrderBy, Fields, MaxRows, StartRow, ResultType (optional)
955
+
956
+ // Valid RunQuery properties:
957
+ // - QueryName (required)
958
+ // - CategoryName, CategoryID, Parameters (optional)`
959
+ });
960
+ break;
865
961
  case 'root-component-props-restriction':
866
962
  suggestions.push({
867
963
  violation: violation.rule,
@@ -913,11 +1009,11 @@ ComponentLinter.universalComponentRules = [
913
1009
  appliesTo: 'all',
914
1010
  test: (ast, componentName) => {
915
1011
  const violations = [];
916
- let hasStateFromProps = false;
917
- let hasSavedUserSettings = false;
918
- let usesUseState = false;
919
- const stateProps = [];
920
- // First pass: check if component expects state from props and uses useState
1012
+ const controlledStateProps = [];
1013
+ const initializationProps = [];
1014
+ const eventHandlers = [];
1015
+ const acceptedProps = new Map();
1016
+ // First pass: collect all props
921
1017
  (0, traverse_1.default)(ast, {
922
1018
  FunctionDeclaration(path) {
923
1019
  if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
@@ -926,14 +1022,13 @@ ComponentLinter.universalComponentRules = [
926
1022
  for (const prop of param.properties) {
927
1023
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
928
1024
  const propName = prop.key.name;
929
- // Check for state-like props
930
- const statePatterns = ['selectedId', 'selectedItemId', 'filters', 'sortBy', 'sortField', 'currentPage', 'activeTab'];
931
- if (statePatterns.some(pattern => propName.includes(pattern))) {
932
- hasStateFromProps = true;
933
- stateProps.push(propName);
934
- }
935
- if (propName === 'savedUserSettings') {
936
- hasSavedUserSettings = true;
1025
+ acceptedProps.set(propName, {
1026
+ line: prop.loc?.start.line || 0,
1027
+ column: prop.loc?.start.column || 0
1028
+ });
1029
+ // Categorize props
1030
+ if (/^on[A-Z]/.test(propName) && propName !== 'onSaveUserSettings') {
1031
+ eventHandlers.push(propName);
937
1032
  }
938
1033
  }
939
1034
  }
@@ -950,42 +1045,119 @@ ComponentLinter.universalComponentRules = [
950
1045
  for (const prop of param.properties) {
951
1046
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
952
1047
  const propName = prop.key.name;
953
- const statePatterns = ['selectedId', 'selectedItemId', 'filters', 'sortBy', 'sortField', 'currentPage', 'activeTab'];
954
- if (statePatterns.some(pattern => propName.includes(pattern))) {
955
- hasStateFromProps = true;
956
- stateProps.push(propName);
957
- }
958
- if (propName === 'savedUserSettings') {
959
- hasSavedUserSettings = true;
1048
+ acceptedProps.set(propName, {
1049
+ line: prop.loc?.start.line || 0,
1050
+ column: prop.loc?.start.column || 0
1051
+ });
1052
+ // Categorize props
1053
+ if (/^on[A-Z]/.test(propName) && propName !== 'onSaveUserSettings') {
1054
+ eventHandlers.push(propName);
960
1055
  }
961
1056
  }
962
1057
  }
963
1058
  }
964
1059
  }
965
1060
  }
966
- },
967
- // Check for useState usage
968
- CallExpression(path) {
969
- const callee = path.node.callee;
970
- if ((t.isIdentifier(callee) && callee.name === 'useState') ||
971
- (t.isMemberExpression(callee) &&
972
- t.isIdentifier(callee.object) && callee.object.name === 'React' &&
973
- t.isIdentifier(callee.property) && callee.property.name === 'useState')) {
974
- usesUseState = true;
975
- }
976
1061
  }
977
1062
  });
978
- // CRITICAL: Components must manage ALL their own state
979
- // State props are NOT allowed - each component is generated separately
980
- if (hasStateFromProps) {
1063
+ // Analyze props for controlled component patterns
1064
+ const controlledPatterns = [
1065
+ { stateProp: 'value', handler: 'onChange' },
1066
+ { stateProp: 'checked', handler: 'onChange' },
1067
+ { stateProp: 'selectedId', handler: 'onSelect' },
1068
+ { stateProp: 'selectedIds', handler: 'onSelectionChange' },
1069
+ { stateProp: 'selectedItems', handler: 'onSelectionChange' },
1070
+ { stateProp: 'activeTab', handler: 'onTabChange' },
1071
+ { stateProp: 'currentPage', handler: 'onPageChange' },
1072
+ { stateProp: 'expanded', handler: 'onExpand' },
1073
+ { stateProp: 'collapsed', handler: 'onCollapse' },
1074
+ { stateProp: 'open', handler: 'onOpenChange' },
1075
+ { stateProp: 'visible', handler: 'onVisibilityChange' }
1076
+ ];
1077
+ // Check each accepted prop
1078
+ for (const [propName, location] of acceptedProps) {
1079
+ // Skip standard props
1080
+ if (['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings'].includes(propName)) {
1081
+ continue;
1082
+ }
1083
+ // Skip data props (arrays, objects that are clearly data)
1084
+ if (propName.endsWith('Data') || propName === 'items' || propName === 'options' ||
1085
+ propName === 'rows' || propName === 'columns' || propName === 'records') {
1086
+ continue;
1087
+ }
1088
+ // Check if it's an initialization prop (allowed)
1089
+ if (propName.startsWith('initial') || propName.startsWith('default')) {
1090
+ initializationProps.push(propName);
1091
+ continue;
1092
+ }
1093
+ // Check if it's a configuration prop (allowed)
1094
+ // Be generous - configuration props are those that configure behavior, not manage state
1095
+ if (propName.endsWith('Config') || propName.endsWith('Settings') || propName.endsWith('Options') ||
1096
+ propName === 'pageSize' || propName === 'maxItems' || propName === 'minValue' || propName === 'maxValue' ||
1097
+ propName === 'placeholder' || propName === 'label' || propName === 'title' || propName === 'disabled' ||
1098
+ propName === 'readonly' || propName === 'required' || propName === 'className' || propName === 'style' ||
1099
+ // Sort/filter configuration when not paired with state management handlers
1100
+ ((propName.includes('sort') || propName.includes('Sort') ||
1101
+ propName.includes('filter') || propName.includes('Filter') ||
1102
+ propName === 'orderBy' || propName === 'groupBy') &&
1103
+ !acceptedProps.has('onSortChange') && !acceptedProps.has('onFilterChange') &&
1104
+ !acceptedProps.has('onChange'))) {
1105
+ continue;
1106
+ }
1107
+ // Check for controlled component pattern (both state prop and handler present)
1108
+ let isControlled = false;
1109
+ for (const pattern of controlledPatterns) {
1110
+ if (propName === pattern.stateProp) {
1111
+ // Check if the corresponding handler exists
1112
+ if (acceptedProps.has(pattern.handler) || eventHandlers.includes(pattern.handler)) {
1113
+ controlledStateProps.push(propName);
1114
+ isControlled = true;
1115
+ break;
1116
+ }
1117
+ // If state prop exists without handler, it's still problematic
1118
+ // unless it's clearly for initialization
1119
+ if (!propName.startsWith('initial') && !propName.startsWith('default')) {
1120
+ controlledStateProps.push(propName);
1121
+ isControlled = true;
1122
+ break;
1123
+ }
1124
+ }
1125
+ }
1126
+ // Check for state-like props that aren't initialization
1127
+ if (!isControlled) {
1128
+ const statePatterns = [
1129
+ 'selectedId', 'selectedItemId', 'selectedItem', 'selection',
1130
+ 'filters', 'sortBy', 'sortField', 'sortDirection', 'orderBy',
1131
+ 'currentPage', 'pageNumber', 'page',
1132
+ 'activeTab', 'activeIndex', 'activeKey',
1133
+ 'expandedItems', 'collapsedItems',
1134
+ 'searchTerm', 'searchQuery', 'query',
1135
+ 'formData', 'formValues', 'values'
1136
+ ];
1137
+ for (const pattern of statePatterns) {
1138
+ if (propName === pattern || propName.toLowerCase() === pattern.toLowerCase()) {
1139
+ // This is a state prop without proper initialization naming
1140
+ controlledStateProps.push(propName);
1141
+ break;
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ // Generate violations for controlled state props
1147
+ if (controlledStateProps.length > 0) {
981
1148
  violations.push({
982
1149
  rule: 'full-state-ownership',
983
- severity: 'error',
1150
+ severity: 'critical', // This is critical as it breaks the architecture
984
1151
  line: 1,
985
1152
  column: 0,
986
- message: `Component "${componentName}" expects state from props (${stateProps.join(', ')}) instead of managing internally. Each component is generated separately and MUST manage ALL its own state.`
1153
+ message: `Component "${componentName}" accepts controlled state props (${controlledStateProps.join(', ')}) instead of managing state internally. Use 'initial*' or 'default*' prefixes for initialization props (e.g., 'initialPage' instead of 'currentPage'). Each component must manage ALL its own state.`
987
1154
  });
988
1155
  }
1156
+ // Add warning for initialization props (informational)
1157
+ if (initializationProps.length > 0 && violations.length === 0) {
1158
+ // This is actually OK, but we can log it for awareness
1159
+ // Don't create a violation, just note it's using the correct pattern
1160
+ }
989
1161
  return violations;
990
1162
  }
991
1163
  },
@@ -1003,7 +1175,7 @@ ComponentLinter.universalComponentRules = [
1003
1175
  t.isIdentifier(callee.property) && callee.property.name === 'useReducer')) {
1004
1176
  violations.push({
1005
1177
  rule: 'no-use-reducer',
1006
- severity: 'error',
1178
+ severity: 'high', // High but not critical - it's a pattern violation
1007
1179
  line: path.node.loc?.start.line || 0,
1008
1180
  column: path.node.loc?.start.column || 0,
1009
1181
  message: `Component "${componentName}" uses useReducer at line ${path.node.loc?.start.line}. Components should manage state with useState and persist important settings with onSaveUserSettings.`,
@@ -1031,7 +1203,7 @@ ComponentLinter.universalComponentRules = [
1031
1203
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1032
1204
  violations.push({
1033
1205
  rule: 'no-data-prop',
1034
- severity: 'error',
1206
+ severity: 'medium', // It's a pattern issue, not critical
1035
1207
  line: prop.loc?.start.line || 0,
1036
1208
  column: prop.loc?.start.column || 0,
1037
1209
  message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
@@ -1053,7 +1225,7 @@ ComponentLinter.universalComponentRules = [
1053
1225
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1054
1226
  violations.push({
1055
1227
  rule: 'no-data-prop',
1056
- severity: 'error',
1228
+ severity: 'critical',
1057
1229
  line: prop.loc?.start.line || 0,
1058
1230
  column: prop.loc?.start.column || 0,
1059
1231
  message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
@@ -1092,7 +1264,7 @@ ComponentLinter.universalComponentRules = [
1092
1264
  if (ephemeralPatterns.some(pattern => key.toLowerCase().includes(pattern))) {
1093
1265
  violations.push({
1094
1266
  rule: 'saved-user-settings-pattern',
1095
- severity: 'warning',
1267
+ severity: 'medium', // Pattern issue but not breaking
1096
1268
  line: prop.loc?.start.line || 0,
1097
1269
  column: prop.loc?.start.column || 0,
1098
1270
  message: `Saving ephemeral UI state "${key}" to savedUserSettings. Only save important user preferences.`
@@ -1133,7 +1305,7 @@ ComponentLinter.universalComponentRules = [
1133
1305
  // Only report if some props are passed (to avoid false positives on non-component JSX)
1134
1306
  violations.push({
1135
1307
  rule: 'pass-standard-props',
1136
- severity: 'error',
1308
+ severity: 'critical',
1137
1309
  line: openingElement.loc?.start.line || 0,
1138
1310
  column: openingElement.loc?.start.column || 0,
1139
1311
  message: `Component "${componentBeingCalled}" is missing required props: ${missingProps.join(', ')}. All components must receive styles, utilities, and components props.`,
@@ -1167,7 +1339,7 @@ ComponentLinter.universalComponentRules = [
1167
1339
  if (componentFunctions.length > 0) {
1168
1340
  violations.push({
1169
1341
  rule: 'no-child-implementation',
1170
- severity: 'error',
1342
+ severity: 'critical',
1171
1343
  line: 1,
1172
1344
  column: 0,
1173
1345
  message: `Root component file contains child component implementations: ${componentFunctions.join(', ')}. Root should only reference child components, not implement them.`,
@@ -1239,7 +1411,7 @@ ComponentLinter.universalComponentRules = [
1239
1411
  if (unusedComponents.length > 0) {
1240
1412
  violations.push({
1241
1413
  rule: 'undefined-component-usage',
1242
- severity: 'warning',
1414
+ severity: 'low',
1243
1415
  line: 1,
1244
1416
  column: 0,
1245
1417
  message: `Component destructures ${unusedComponents.join(', ')} from components prop but never uses them. These may be missing from the component spec's dependencies array.`
@@ -1267,7 +1439,7 @@ ComponentLinter.universalComponentRules = [
1267
1439
  if (/\[\d+\]\.\w+/.test(code)) {
1268
1440
  violations.push({
1269
1441
  rule: 'unsafe-array-access',
1270
- severity: 'error',
1442
+ severity: 'critical',
1271
1443
  line: path.node.loc?.start.line || 0,
1272
1444
  column: path.node.loc?.start.column || 0,
1273
1445
  message: `Unsafe array access: ${code}. Check array bounds before accessing elements.`,
@@ -1300,7 +1472,7 @@ ComponentLinter.universalComponentRules = [
1300
1472
  if (!hasInitialValue) {
1301
1473
  violations.push({
1302
1474
  rule: 'array-reduce-safety',
1303
- severity: 'warning',
1475
+ severity: 'low',
1304
1476
  line: path.node.loc?.start.line || 0,
1305
1477
  column: path.node.loc?.start.column || 0,
1306
1478
  message: `reduce() without initial value may fail on empty arrays: ${code}`,
@@ -1313,7 +1485,7 @@ ComponentLinter.universalComponentRules = [
1313
1485
  (t.isIdentifier(arrayExpression.property) && arrayExpression.computed))) {
1314
1486
  violations.push({
1315
1487
  rule: 'array-reduce-safety',
1316
- severity: 'error',
1488
+ severity: 'critical',
1317
1489
  line: path.node.loc?.start.line || 0,
1318
1490
  column: path.node.loc?.start.column || 0,
1319
1491
  message: `reduce() on array element access is unsafe: ${code}`,
@@ -1448,7 +1620,7 @@ ComponentLinter.universalComponentRules = [
1448
1620
  if (relatedHandlers.length > 0) {
1449
1621
  violations.push({
1450
1622
  rule: 'parent-event-callback-usage',
1451
- severity: 'error',
1623
+ severity: 'critical',
1452
1624
  line: location.line,
1453
1625
  column: location.column,
1454
1626
  message: `Component receives '${callbackName}' event callback but never invokes it. Found state updates in ${relatedHandlers.join(', ')} but parent is not notified.`,
@@ -1565,7 +1737,7 @@ ComponentLinter.universalComponentRules = [
1565
1737
  const transformedName = Array.from(transformation.transformedProps).find(t => t.toLowerCase() === accessedProp.toLowerCase());
1566
1738
  violations.push({
1567
1739
  rule: 'property-name-consistency',
1568
- severity: 'error',
1740
+ severity: 'critical',
1569
1741
  line: transformation.location.line,
1570
1742
  column: transformation.location.column,
1571
1743
  message: `Property name mismatch: data transformed with different casing. Accessing '${accessedProp}' but property was transformed to '${transformedName || 'different name'}'`,
@@ -1603,7 +1775,7 @@ ComponentLinter.universalComponentRules = [
1603
1775
  if (!hasDebounce && !hasTimeout) {
1604
1776
  violations.push({
1605
1777
  rule: 'noisy-settings-updates',
1606
- severity: 'error',
1778
+ severity: 'critical',
1607
1779
  line: path.node.loc?.start.line || 0,
1608
1780
  column: path.node.loc?.start.column || 0,
1609
1781
  message: `Saving settings on every change/keystroke. Save on blur, submit, or after debouncing.`
@@ -1639,7 +1811,7 @@ ComponentLinter.universalComponentRules = [
1639
1811
  if (hasSetState && hasPropDeps && !bodyString.includes('async')) {
1640
1812
  violations.push({
1641
1813
  rule: 'prop-state-sync',
1642
- severity: 'error',
1814
+ severity: 'critical',
1643
1815
  line: path.node.loc?.start.line || 0,
1644
1816
  column: path.node.loc?.start.column || 0,
1645
1817
  message: 'Syncing props to internal state with useEffect creates dual state management',
@@ -1693,7 +1865,7 @@ ComponentLinter.universalComponentRules = [
1693
1865
  if (!funcName || funcName === componentName) {
1694
1866
  violations.push({
1695
1867
  rule: 'performance-memoization',
1696
- severity: 'warning',
1868
+ severity: 'low', // Just a suggestion, not mandatory
1697
1869
  line: path.node.loc?.start.line || 0,
1698
1870
  column: path.node.loc?.start.column || 0,
1699
1871
  message: `Expensive ${method} operation without memoization. Consider using useMemo.`,
@@ -1717,7 +1889,7 @@ ComponentLinter.universalComponentRules = [
1717
1889
  if (!hasVariables || hasVariables.length < 3) { // Allow some property names
1718
1890
  violations.push({
1719
1891
  rule: 'performance-memoization',
1720
- severity: 'warning',
1892
+ severity: 'low', // Just a suggestion
1721
1893
  line: path.node.loc?.start.line || 0,
1722
1894
  column: path.node.loc?.start.column || 0,
1723
1895
  message: 'Static array/object recreated on every render. Consider using useMemo.',
@@ -1755,7 +1927,7 @@ ComponentLinter.universalComponentRules = [
1755
1927
  if (childPatterns.some(pattern => pattern.test(stateName))) {
1756
1928
  violations.push({
1757
1929
  rule: 'child-state-management',
1758
- severity: 'error',
1930
+ severity: 'critical',
1759
1931
  line: path.node.loc?.start.line || 0,
1760
1932
  column: path.node.loc?.start.column || 0,
1761
1933
  message: `Component trying to manage child component state: ${stateName}. Child components manage their own state!`,
@@ -1790,7 +1962,7 @@ ComponentLinter.universalComponentRules = [
1790
1962
  funcName.includes('handleSort') || funcName.includes('handleFilter'))) {
1791
1963
  violations.push({
1792
1964
  rule: 'server-reload-on-client-operation',
1793
- severity: 'error',
1965
+ severity: 'critical',
1794
1966
  line: path.node.loc?.start.line || 0,
1795
1967
  column: path.node.loc?.start.column || 0,
1796
1968
  message: 'Reloading data from server on sort/filter. Use useMemo for client-side operations.',
@@ -1804,6 +1976,130 @@ ComponentLinter.universalComponentRules = [
1804
1976
  return violations;
1805
1977
  }
1806
1978
  },
1979
+ {
1980
+ name: 'runview-runquery-valid-properties',
1981
+ appliesTo: 'all',
1982
+ test: (ast, componentName) => {
1983
+ const violations = [];
1984
+ // Valid properties for RunView/RunViews
1985
+ const validRunViewProps = new Set([
1986
+ 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
1987
+ 'MaxRows', 'StartRow', 'ResultType'
1988
+ ]);
1989
+ // Valid properties for RunQuery
1990
+ const validRunQueryProps = new Set([
1991
+ 'QueryName', 'CategoryName', 'CategoryID', 'Parameters'
1992
+ ]);
1993
+ (0, traverse_1.default)(ast, {
1994
+ CallExpression(path) {
1995
+ const callee = path.node.callee;
1996
+ // Check for utilities.rv.RunView or utilities.rv.RunViews
1997
+ if (t.isMemberExpression(callee) &&
1998
+ t.isMemberExpression(callee.object) &&
1999
+ t.isIdentifier(callee.object.object) &&
2000
+ callee.object.object.name === 'utilities' &&
2001
+ t.isIdentifier(callee.object.property) &&
2002
+ callee.object.property.name === 'rv' &&
2003
+ t.isIdentifier(callee.property)) {
2004
+ const methodName = callee.property.name;
2005
+ if (methodName === 'RunView' || methodName === 'RunViews') {
2006
+ // Get the config object(s)
2007
+ let configs = [];
2008
+ if (methodName === 'RunViews' && path.node.arguments[0]) {
2009
+ // RunViews takes an array of configs
2010
+ if (t.isArrayExpression(path.node.arguments[0])) {
2011
+ configs = path.node.arguments[0].elements
2012
+ .filter((e) => t.isObjectExpression(e));
2013
+ }
2014
+ }
2015
+ else if (methodName === 'RunView' && path.node.arguments[0]) {
2016
+ // RunView takes a single config
2017
+ if (t.isObjectExpression(path.node.arguments[0])) {
2018
+ configs = [path.node.arguments[0]];
2019
+ }
2020
+ }
2021
+ // Check each config for invalid properties
2022
+ for (const config of configs) {
2023
+ for (const prop of config.properties) {
2024
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2025
+ const propName = prop.key.name;
2026
+ if (!validRunViewProps.has(propName)) {
2027
+ // Special error messages for common mistakes
2028
+ let message = `Invalid property '${propName}' on ${methodName}. Valid properties: ${Array.from(validRunViewProps).join(', ')}`;
2029
+ let fix = `Remove '${propName}' property`;
2030
+ if (propName === 'Parameters') {
2031
+ message = `${methodName} does not support 'Parameters'. Use 'ExtraFilter' for WHERE clauses.`;
2032
+ fix = `Replace 'Parameters' with 'ExtraFilter' and format as SQL WHERE clause`;
2033
+ }
2034
+ else if (propName === 'GroupBy') {
2035
+ message = `${methodName} does not support 'GroupBy'. Use RunQuery with a pre-defined query for aggregations.`;
2036
+ fix = `Remove 'GroupBy' and use RunQuery instead for aggregated data`;
2037
+ }
2038
+ else if (propName === 'Having') {
2039
+ message = `${methodName} does not support 'Having'. Use RunQuery with a pre-defined query.`;
2040
+ fix = `Remove 'Having' and use RunQuery instead`;
2041
+ }
2042
+ violations.push({
2043
+ rule: 'runview-runquery-valid-properties',
2044
+ severity: 'critical',
2045
+ line: prop.loc?.start.line || 0,
2046
+ column: prop.loc?.start.column || 0,
2047
+ message,
2048
+ code: `${propName}: ...`
2049
+ });
2050
+ }
2051
+ }
2052
+ }
2053
+ }
2054
+ }
2055
+ }
2056
+ // Check for utilities.rq.RunQuery
2057
+ if (t.isMemberExpression(callee) &&
2058
+ t.isMemberExpression(callee.object) &&
2059
+ t.isIdentifier(callee.object.object) &&
2060
+ callee.object.object.name === 'utilities' &&
2061
+ t.isIdentifier(callee.object.property) &&
2062
+ callee.object.property.name === 'rq' &&
2063
+ t.isIdentifier(callee.property) &&
2064
+ callee.property.name === 'RunQuery') {
2065
+ if (path.node.arguments[0] && t.isObjectExpression(path.node.arguments[0])) {
2066
+ const config = path.node.arguments[0];
2067
+ for (const prop of config.properties) {
2068
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
2069
+ const propName = prop.key.name;
2070
+ if (!validRunQueryProps.has(propName)) {
2071
+ let message = `Invalid property '${propName}' on RunQuery. Valid properties: ${Array.from(validRunQueryProps).join(', ')}`;
2072
+ let fix = `Remove '${propName}' property`;
2073
+ if (propName === 'ExtraFilter') {
2074
+ message = `RunQuery does not support 'ExtraFilter'. WHERE clauses should be in the pre-defined query or passed as Parameters.`;
2075
+ fix = `Remove 'ExtraFilter'. Add WHERE logic to the query definition or pass as Parameters`;
2076
+ }
2077
+ else if (propName === 'Fields') {
2078
+ message = `RunQuery does not support 'Fields'. The query definition determines returned fields.`;
2079
+ fix = `Remove 'Fields'. Modify the query definition to return desired fields`;
2080
+ }
2081
+ else if (propName === 'OrderBy') {
2082
+ message = `RunQuery does not support 'OrderBy'. ORDER BY should be in the query definition.`;
2083
+ fix = `Remove 'OrderBy'. Add ORDER BY to the query definition`;
2084
+ }
2085
+ violations.push({
2086
+ rule: 'runview-runquery-valid-properties',
2087
+ severity: 'critical',
2088
+ line: prop.loc?.start.line || 0,
2089
+ column: prop.loc?.start.column || 0,
2090
+ message,
2091
+ code: `${propName}: ...`
2092
+ });
2093
+ }
2094
+ }
2095
+ }
2096
+ }
2097
+ }
2098
+ }
2099
+ });
2100
+ return violations;
2101
+ }
2102
+ },
1807
2103
  {
1808
2104
  name: 'root-component-props-restriction',
1809
2105
  appliesTo: 'root',
@@ -1834,7 +2130,7 @@ ComponentLinter.universalComponentRules = [
1834
2130
  if (invalidProps.length > 0) {
1835
2131
  violations.push({
1836
2132
  rule: 'root-component-props-restriction',
1837
- severity: 'error',
2133
+ severity: 'critical',
1838
2134
  line: path.node.loc?.start.line || 0,
1839
2135
  column: path.node.loc?.start.column || 0,
1840
2136
  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().`
@@ -1864,7 +2160,7 @@ ComponentLinter.universalComponentRules = [
1864
2160
  if (invalidProps.length > 0) {
1865
2161
  violations.push({
1866
2162
  rule: 'root-component-props-restriction',
1867
- severity: 'error',
2163
+ severity: 'critical',
1868
2164
  line: path.node.loc?.start.line || 0,
1869
2165
  column: path.node.loc?.start.column || 0,
1870
2166
  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().`