@memberjunction/react-test-harness 2.88.0 → 2.90.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.
@@ -56,7 +56,7 @@ class ComponentLinter {
56
56
  const violations = [];
57
57
  // Run each rule
58
58
  for (const rule of rules) {
59
- const ruleViolations = rule.test(ast, componentName);
59
+ const ruleViolations = rule.test(ast, componentName, componentSpec);
60
60
  violations.push(...ruleViolations);
61
61
  }
62
62
  // Add data requirements validation if componentSpec is provided
@@ -1003,168 +1003,10 @@ function RootComponent({ utilities, styles, components, callbacks, savedUserSett
1003
1003
  exports.ComponentLinter = ComponentLinter;
1004
1004
  // Universal rules that apply to all components with SavedUserSettings pattern
1005
1005
  ComponentLinter.universalComponentRules = [
1006
- // State Management Rules
1007
- {
1008
- name: 'full-state-ownership',
1009
- appliesTo: 'all',
1010
- test: (ast, componentName) => {
1011
- const violations = [];
1012
- const controlledStateProps = [];
1013
- const initializationProps = [];
1014
- const eventHandlers = [];
1015
- const acceptedProps = new Map();
1016
- // First pass: collect all props
1017
- (0, traverse_1.default)(ast, {
1018
- FunctionDeclaration(path) {
1019
- if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
1020
- const param = path.node.params[0];
1021
- if (t.isObjectPattern(param)) {
1022
- for (const prop of param.properties) {
1023
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1024
- const propName = prop.key.name;
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);
1032
- }
1033
- }
1034
- }
1035
- }
1036
- }
1037
- },
1038
- // Also check arrow functions
1039
- VariableDeclarator(path) {
1040
- if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
1041
- const init = path.node.init;
1042
- if (t.isArrowFunctionExpression(init) && init.params[0]) {
1043
- const param = init.params[0];
1044
- if (t.isObjectPattern(param)) {
1045
- for (const prop of param.properties) {
1046
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1047
- const propName = prop.key.name;
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);
1055
- }
1056
- }
1057
- }
1058
- }
1059
- }
1060
- }
1061
- }
1062
- });
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) {
1148
- violations.push({
1149
- rule: 'full-state-ownership',
1150
- severity: 'critical', // This is critical as it breaks the architecture
1151
- line: 1,
1152
- column: 0,
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.`
1154
- });
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
- }
1161
- return violations;
1162
- }
1163
- },
1164
1006
  {
1165
1007
  name: 'no-use-reducer',
1166
1008
  appliesTo: 'all',
1167
- test: (ast, componentName) => {
1009
+ test: (ast, componentName, componentSpec) => {
1168
1010
  const violations = [];
1169
1011
  (0, traverse_1.default)(ast, {
1170
1012
  CallExpression(path) {
@@ -1191,7 +1033,7 @@ ComponentLinter.universalComponentRules = [
1191
1033
  {
1192
1034
  name: 'no-data-prop',
1193
1035
  appliesTo: 'all',
1194
- test: (ast, componentName) => {
1036
+ test: (ast, componentName, componentSpec) => {
1195
1037
  const violations = [];
1196
1038
  (0, traverse_1.default)(ast, {
1197
1039
  // Check function parameters for 'data' prop
@@ -1244,7 +1086,7 @@ ComponentLinter.universalComponentRules = [
1244
1086
  {
1245
1087
  name: 'saved-user-settings-pattern',
1246
1088
  appliesTo: 'all',
1247
- test: (ast, componentName) => {
1089
+ test: (ast, componentName, componentSpec) => {
1248
1090
  const violations = [];
1249
1091
  // Check for improper onSaveUserSettings usage
1250
1092
  (0, traverse_1.default)(ast, {
@@ -1283,14 +1125,45 @@ ComponentLinter.universalComponentRules = [
1283
1125
  {
1284
1126
  name: 'pass-standard-props',
1285
1127
  appliesTo: 'all',
1286
- test: (ast, componentName) => {
1128
+ test: (ast, componentName, componentSpec) => {
1287
1129
  const violations = [];
1288
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
+ (0, traverse_1.default)(ast, {
1143
+ 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
+ }
1157
+ }
1158
+ }
1159
+ });
1160
+ // Now check only our components for standard props
1289
1161
  (0, traverse_1.default)(ast, {
1290
1162
  JSXElement(path) {
1291
1163
  const openingElement = path.node.openingElement;
1292
- // Check if this looks like a component (capitalized name)
1293
- if (t.isJSXIdentifier(openingElement.name) && /^[A-Z]/.test(openingElement.name.name)) {
1164
+ // Only check if it's one of our components
1165
+ if (t.isJSXIdentifier(openingElement.name) &&
1166
+ ourComponentNames.has(openingElement.name.name)) {
1294
1167
  const componentBeingCalled = openingElement.name.name;
1295
1168
  const passedProps = new Set();
1296
1169
  // Collect all props being passed
@@ -1301,14 +1174,13 @@ ComponentLinter.universalComponentRules = [
1301
1174
  }
1302
1175
  // Check if required props are missing
1303
1176
  const missingProps = requiredProps.filter(prop => !passedProps.has(prop));
1304
- if (missingProps.length > 0 && passedProps.size > 0) {
1305
- // Only report if some props are passed (to avoid false positives on non-component JSX)
1177
+ if (missingProps.length > 0) {
1306
1178
  violations.push({
1307
1179
  rule: 'pass-standard-props',
1308
1180
  severity: 'critical',
1309
1181
  line: openingElement.loc?.start.line || 0,
1310
1182
  column: openingElement.loc?.start.column || 0,
1311
- message: `Component "${componentBeingCalled}" is missing required props: ${missingProps.join(', ')}. All components must receive styles, utilities, and components props.`,
1183
+ message: `Component "${componentBeingCalled}" is missing required props: ${missingProps.join(', ')}. All child components must receive styles, utilities, and components props.`,
1312
1184
  code: `<${componentBeingCalled} ... />`
1313
1185
  });
1314
1186
  }
@@ -1321,7 +1193,7 @@ ComponentLinter.universalComponentRules = [
1321
1193
  {
1322
1194
  name: 'no-child-implementation',
1323
1195
  appliesTo: 'root',
1324
- test: (ast, componentName) => {
1196
+ test: (ast, componentName, componentSpec) => {
1325
1197
  const violations = [];
1326
1198
  const rootFunctionName = componentName;
1327
1199
  const declaredFunctions = [];
@@ -1351,7 +1223,7 @@ ComponentLinter.universalComponentRules = [
1351
1223
  {
1352
1224
  name: 'undefined-component-usage',
1353
1225
  appliesTo: 'all',
1354
- test: (ast, componentName) => {
1226
+ test: (ast, componentName, componentSpec) => {
1355
1227
  const violations = [];
1356
1228
  const componentsFromProps = new Set();
1357
1229
  const componentsUsedInJSX = new Set();
@@ -1424,7 +1296,7 @@ ComponentLinter.universalComponentRules = [
1424
1296
  {
1425
1297
  name: 'unsafe-array-access',
1426
1298
  appliesTo: 'all',
1427
- test: (ast, componentName) => {
1299
+ test: (ast, componentName, componentSpec) => {
1428
1300
  const violations = [];
1429
1301
  (0, traverse_1.default)(ast, {
1430
1302
  MemberExpression(path) {
@@ -1456,7 +1328,7 @@ ComponentLinter.universalComponentRules = [
1456
1328
  {
1457
1329
  name: 'array-reduce-safety',
1458
1330
  appliesTo: 'all',
1459
- test: (ast, componentName) => {
1331
+ test: (ast, componentName, componentSpec) => {
1460
1332
  const violations = [];
1461
1333
  (0, traverse_1.default)(ast, {
1462
1334
  CallExpression(path) {
@@ -1498,144 +1370,144 @@ ComponentLinter.universalComponentRules = [
1498
1370
  return violations;
1499
1371
  }
1500
1372
  },
1501
- {
1502
- name: 'parent-event-callback-usage',
1503
- appliesTo: 'child',
1504
- test: (ast, componentName) => {
1505
- const violations = [];
1506
- const eventCallbacks = new Map();
1507
- const callbackInvocations = new Set();
1508
- const stateUpdateHandlers = new Map(); // handler -> state updates
1509
- // First pass: collect event callback props (onSelect, onChange, etc.)
1510
- (0, traverse_1.default)(ast, {
1511
- FunctionDeclaration(path) {
1512
- if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
1513
- const param = path.node.params[0];
1514
- if (t.isObjectPattern(param)) {
1515
- for (const prop of param.properties) {
1516
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1517
- const propName = prop.key.name;
1518
- // Check for event callback patterns
1519
- if (/^on[A-Z]/.test(propName) &&
1520
- propName !== 'onSaveUserSettings' &&
1521
- !propName.includes('StateChanged')) {
1522
- eventCallbacks.set(propName, {
1523
- line: prop.loc?.start.line || 0,
1524
- column: prop.loc?.start.column || 0
1525
- });
1526
- }
1527
- }
1528
- }
1529
- }
1530
- }
1531
- },
1532
- // Also check arrow function components
1533
- VariableDeclarator(path) {
1534
- if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
1535
- const init = path.node.init;
1536
- if (t.isArrowFunctionExpression(init) && init.params[0]) {
1537
- const param = init.params[0];
1538
- if (t.isObjectPattern(param)) {
1539
- for (const prop of param.properties) {
1540
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1541
- const propName = prop.key.name;
1542
- if (/^on[A-Z]/.test(propName) &&
1543
- propName !== 'onSaveUserSettings' &&
1544
- !propName.includes('StateChanged')) {
1545
- eventCallbacks.set(propName, {
1546
- line: prop.loc?.start.line || 0,
1547
- column: prop.loc?.start.column || 0
1548
- });
1549
- }
1550
- }
1551
- }
1552
- }
1553
- }
1554
- }
1555
- }
1556
- });
1557
- // Second pass: check if callbacks are invoked in event handlers
1558
- (0, traverse_1.default)(ast, {
1559
- CallExpression(path) {
1560
- // Check for callback invocations
1561
- if (t.isIdentifier(path.node.callee)) {
1562
- const callbackName = path.node.callee.name;
1563
- if (eventCallbacks.has(callbackName)) {
1564
- callbackInvocations.add(callbackName);
1565
- }
1566
- }
1567
- // Check for state updates (setSelectedId, setFilters, etc.)
1568
- if (t.isIdentifier(path.node.callee) && /^set[A-Z]/.test(path.node.callee.name)) {
1569
- // Find the containing function
1570
- let containingFunction = path.getFunctionParent();
1571
- if (containingFunction) {
1572
- const funcName = ComponentLinter.getFunctionName(containingFunction);
1573
- if (funcName) {
1574
- if (!stateUpdateHandlers.has(funcName)) {
1575
- stateUpdateHandlers.set(funcName, []);
1576
- }
1577
- stateUpdateHandlers.get(funcName).push(path.node.callee.name);
1578
- }
1579
- }
1580
- }
1581
- },
1582
- // Check conditional callback invocations
1583
- IfStatement(path) {
1584
- if (t.isBlockStatement(path.node.consequent)) {
1585
- // Check if the condition tests for callback existence
1586
- if (t.isIdentifier(path.node.test)) {
1587
- const callbackName = path.node.test.name;
1588
- if (eventCallbacks.has(callbackName)) {
1589
- // Check if callback is invoked in the block
1590
- let hasInvocation = false;
1591
- path.traverse({
1592
- CallExpression(innerPath) {
1593
- if (t.isIdentifier(innerPath.node.callee) &&
1594
- innerPath.node.callee.name === callbackName) {
1595
- hasInvocation = true;
1596
- callbackInvocations.add(callbackName);
1597
- }
1598
- }
1599
- });
1600
- }
1601
- }
1602
- }
1603
- }
1604
- });
1605
- // Check for unused callbacks that have related state updates
1606
- for (const [callbackName, location] of eventCallbacks) {
1607
- if (!callbackInvocations.has(callbackName)) {
1608
- // Try to find related state update handlers
1609
- const relatedHandlers = [];
1610
- const expectedStateName = callbackName.replace(/^on/, '').replace(/Change$|Select$/, '');
1611
- for (const [handlerName, stateUpdates] of stateUpdateHandlers) {
1612
- for (const stateUpdate of stateUpdates) {
1613
- if (stateUpdate.toLowerCase().includes(expectedStateName.toLowerCase()) ||
1614
- handlerName.toLowerCase().includes(expectedStateName.toLowerCase())) {
1615
- relatedHandlers.push(handlerName);
1616
- break;
1617
- }
1618
- }
1619
- }
1620
- if (relatedHandlers.length > 0) {
1621
- violations.push({
1622
- rule: 'parent-event-callback-usage',
1623
- severity: 'critical',
1624
- line: location.line,
1625
- column: location.column,
1626
- message: `Component receives '${callbackName}' event callback but never invokes it. Found state updates in ${relatedHandlers.join(', ')} but parent is not notified.`,
1627
- code: `Missing: if (${callbackName}) ${callbackName}(...)`
1628
- });
1629
- }
1630
- }
1631
- }
1632
- return violations;
1633
- }
1634
- },
1373
+ // {
1374
+ // name: 'parent-event-callback-usage',
1375
+ // appliesTo: 'child',
1376
+ // test: (ast: t.File, componentName: string) => {
1377
+ // const violations: Violation[] = [];
1378
+ // const eventCallbacks = new Map<string, { line: number; column: number }>();
1379
+ // const callbackInvocations = new Set<string>();
1380
+ // const stateUpdateHandlers = new Map<string, string[]>(); // handler -> state updates
1381
+ // // First pass: collect event callback props (onSelect, onChange, etc.)
1382
+ // traverse(ast, {
1383
+ // FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {
1384
+ // if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
1385
+ // const param = path.node.params[0];
1386
+ // if (t.isObjectPattern(param)) {
1387
+ // for (const prop of param.properties) {
1388
+ // if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1389
+ // const propName = prop.key.name;
1390
+ // // Check for event callback patterns
1391
+ // if (/^on[A-Z]/.test(propName) &&
1392
+ // propName !== 'onSaveUserSettings' &&
1393
+ // !propName.includes('StateChanged')) {
1394
+ // eventCallbacks.set(propName, {
1395
+ // line: prop.loc?.start.line || 0,
1396
+ // column: prop.loc?.start.column || 0
1397
+ // });
1398
+ // }
1399
+ // }
1400
+ // }
1401
+ // }
1402
+ // }
1403
+ // },
1404
+ // // Also check arrow function components
1405
+ // VariableDeclarator(path: NodePath<t.VariableDeclarator>) {
1406
+ // if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
1407
+ // const init = path.node.init;
1408
+ // if (t.isArrowFunctionExpression(init) && init.params[0]) {
1409
+ // const param = init.params[0];
1410
+ // if (t.isObjectPattern(param)) {
1411
+ // for (const prop of param.properties) {
1412
+ // if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1413
+ // const propName = prop.key.name;
1414
+ // if (/^on[A-Z]/.test(propName) &&
1415
+ // propName !== 'onSaveUserSettings' &&
1416
+ // !propName.includes('StateChanged')) {
1417
+ // eventCallbacks.set(propName, {
1418
+ // line: prop.loc?.start.line || 0,
1419
+ // column: prop.loc?.start.column || 0
1420
+ // });
1421
+ // }
1422
+ // }
1423
+ // }
1424
+ // }
1425
+ // }
1426
+ // }
1427
+ // }
1428
+ // });
1429
+ // // Second pass: check if callbacks are invoked in event handlers
1430
+ // traverse(ast, {
1431
+ // CallExpression(path: NodePath<t.CallExpression>) {
1432
+ // // Check for callback invocations
1433
+ // if (t.isIdentifier(path.node.callee)) {
1434
+ // const callbackName = path.node.callee.name;
1435
+ // if (eventCallbacks.has(callbackName)) {
1436
+ // callbackInvocations.add(callbackName);
1437
+ // }
1438
+ // }
1439
+ // // Check for state updates (setSelectedId, setFilters, etc.)
1440
+ // if (t.isIdentifier(path.node.callee) && /^set[A-Z]/.test(path.node.callee.name)) {
1441
+ // // Find the containing function
1442
+ // let containingFunction = path.getFunctionParent();
1443
+ // if (containingFunction) {
1444
+ // const funcName = ComponentLinter.getFunctionName(containingFunction);
1445
+ // if (funcName) {
1446
+ // if (!stateUpdateHandlers.has(funcName)) {
1447
+ // stateUpdateHandlers.set(funcName, []);
1448
+ // }
1449
+ // stateUpdateHandlers.get(funcName)!.push(path.node.callee.name);
1450
+ // }
1451
+ // }
1452
+ // }
1453
+ // },
1454
+ // // Check conditional callback invocations
1455
+ // IfStatement(path: NodePath<t.IfStatement>) {
1456
+ // if (t.isBlockStatement(path.node.consequent)) {
1457
+ // // Check if the condition tests for callback existence
1458
+ // if (t.isIdentifier(path.node.test)) {
1459
+ // const callbackName = path.node.test.name;
1460
+ // if (eventCallbacks.has(callbackName)) {
1461
+ // // Check if callback is invoked in the block
1462
+ // let hasInvocation = false;
1463
+ // path.traverse({
1464
+ // CallExpression(innerPath: NodePath<t.CallExpression>) {
1465
+ // if (t.isIdentifier(innerPath.node.callee) &&
1466
+ // innerPath.node.callee.name === callbackName) {
1467
+ // hasInvocation = true;
1468
+ // callbackInvocations.add(callbackName);
1469
+ // }
1470
+ // }
1471
+ // });
1472
+ // }
1473
+ // }
1474
+ // }
1475
+ // }
1476
+ // });
1477
+ // // Check for unused callbacks that have related state updates
1478
+ // for (const [callbackName, location] of eventCallbacks) {
1479
+ // if (!callbackInvocations.has(callbackName)) {
1480
+ // // Try to find related state update handlers
1481
+ // const relatedHandlers: string[] = [];
1482
+ // const expectedStateName = callbackName.replace(/^on/, '').replace(/Change$|Select$/, '');
1483
+ // for (const [handlerName, stateUpdates] of stateUpdateHandlers) {
1484
+ // for (const stateUpdate of stateUpdates) {
1485
+ // if (stateUpdate.toLowerCase().includes(expectedStateName.toLowerCase()) ||
1486
+ // handlerName.toLowerCase().includes(expectedStateName.toLowerCase())) {
1487
+ // relatedHandlers.push(handlerName);
1488
+ // break;
1489
+ // }
1490
+ // }
1491
+ // }
1492
+ // if (relatedHandlers.length > 0) {
1493
+ // violations.push({
1494
+ // rule: 'parent-event-callback-usage',
1495
+ // severity: 'critical',
1496
+ // line: location.line,
1497
+ // column: location.column,
1498
+ // message: `Component receives '${callbackName}' event callback but never invokes it. Found state updates in ${relatedHandlers.join(', ')} but parent is not notified.`,
1499
+ // code: `Missing: if (${callbackName}) ${callbackName}(...)`
1500
+ // });
1501
+ // }
1502
+ // }
1503
+ // }
1504
+ // return violations;
1505
+ // }
1506
+ // },
1635
1507
  {
1636
1508
  name: 'property-name-consistency',
1637
1509
  appliesTo: 'all',
1638
- test: (ast, componentName) => {
1510
+ test: (ast, componentName, componentSpec) => {
1639
1511
  const violations = [];
1640
1512
  const dataTransformations = new Map();
1641
1513
  const propertyAccesses = new Map(); // variable -> accessed properties
@@ -1755,7 +1627,7 @@ ComponentLinter.universalComponentRules = [
1755
1627
  {
1756
1628
  name: 'noisy-settings-updates',
1757
1629
  appliesTo: 'all',
1758
- test: (ast, componentName) => {
1630
+ test: (ast, componentName, componentSpec) => {
1759
1631
  const violations = [];
1760
1632
  (0, traverse_1.default)(ast, {
1761
1633
  CallExpression(path) {
@@ -1793,7 +1665,7 @@ ComponentLinter.universalComponentRules = [
1793
1665
  {
1794
1666
  name: 'prop-state-sync',
1795
1667
  appliesTo: 'all',
1796
- test: (ast, componentName) => {
1668
+ test: (ast, componentName, componentSpec) => {
1797
1669
  const violations = [];
1798
1670
  (0, traverse_1.default)(ast, {
1799
1671
  CallExpression(path) {
@@ -1828,7 +1700,7 @@ ComponentLinter.universalComponentRules = [
1828
1700
  {
1829
1701
  name: 'performance-memoization',
1830
1702
  appliesTo: 'all',
1831
- test: (ast, componentName) => {
1703
+ test: (ast, componentName, componentSpec) => {
1832
1704
  const violations = [];
1833
1705
  const memoizedValues = new Set();
1834
1706
  // Collect memoized values
@@ -1906,7 +1778,7 @@ ComponentLinter.universalComponentRules = [
1906
1778
  {
1907
1779
  name: 'child-state-management',
1908
1780
  appliesTo: 'all',
1909
- test: (ast, componentName) => {
1781
+ test: (ast, componentName, componentSpec) => {
1910
1782
  const violations = [];
1911
1783
  (0, traverse_1.default)(ast, {
1912
1784
  CallExpression(path) {
@@ -1945,7 +1817,7 @@ ComponentLinter.universalComponentRules = [
1945
1817
  {
1946
1818
  name: 'server-reload-on-client-operation',
1947
1819
  appliesTo: 'all',
1948
- test: (ast, componentName) => {
1820
+ test: (ast, componentName, componentSpec) => {
1949
1821
  const violations = [];
1950
1822
  (0, traverse_1.default)(ast, {
1951
1823
  CallExpression(path) {
@@ -1979,16 +1851,17 @@ ComponentLinter.universalComponentRules = [
1979
1851
  {
1980
1852
  name: 'runview-runquery-valid-properties',
1981
1853
  appliesTo: 'all',
1982
- test: (ast, componentName) => {
1854
+ test: (ast, componentName, componentSpec) => {
1983
1855
  const violations = [];
1984
1856
  // Valid properties for RunView/RunViews
1985
1857
  const validRunViewProps = new Set([
1986
- 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
1987
- 'MaxRows', 'StartRow', 'ResultType'
1858
+ 'ViewID', 'ViewName', 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
1859
+ 'MaxRows', 'StartRow', 'ResultType', 'UserSearchString', 'ForceAuditLog', 'AuditLogDescription',
1860
+ 'ResultType'
1988
1861
  ]);
1989
1862
  // Valid properties for RunQuery
1990
1863
  const validRunQueryProps = new Set([
1991
- 'QueryName', 'CategoryName', 'CategoryID', 'Parameters'
1864
+ 'QueryID', 'QueryName', 'CategoryID', 'CategoryPath', 'Parameters', 'MaxRows', 'StartRow', 'ForceAuditLog', 'AuditLogDescription'
1992
1865
  ]);
1993
1866
  (0, traverse_1.default)(ast, {
1994
1867
  CallExpression(path) {
@@ -2103,7 +1976,7 @@ ComponentLinter.universalComponentRules = [
2103
1976
  {
2104
1977
  name: 'root-component-props-restriction',
2105
1978
  appliesTo: 'root',
2106
- test: (ast, componentName) => {
1979
+ test: (ast, componentName, componentSpec) => {
2107
1980
  const violations = [];
2108
1981
  const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
2109
1982
  // This rule applies when testing root components