@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.
- package/README.md +98 -0
- package/dist/lib/component-linter.d.ts +2 -1
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +190 -317
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +11 -0
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +323 -73
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/test-harness.d.ts +31 -0
- package/dist/lib/test-harness.d.ts.map +1 -1
- package/dist/lib/test-harness.js +53 -0
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
-
//
|
|
1293
|
-
if (t.isJSXIdentifier(openingElement.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
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
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
|
-
'
|
|
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
|