@memberjunction/react-test-harness 2.96.0 → 2.97.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.
@@ -222,14 +222,16 @@ class ComponentLinter {
222
222
  }
223
223
  // If we have critical syntax errors, return immediately with those
224
224
  if (syntaxViolations.length > 0) {
225
+ // Add suggestions directly to syntax violations
226
+ this.generateSyntaxErrorSuggestions(syntaxViolations);
225
227
  return {
226
228
  success: false,
227
229
  violations: syntaxViolations,
228
- suggestions: this.generateSyntaxErrorSuggestions(syntaxViolations),
229
230
  criticalCount: syntaxViolations.length,
230
231
  highCount: 0,
231
232
  mediumCount: 0,
232
- lowCount: 0
233
+ lowCount: 0,
234
+ hasErrors: true
233
235
  };
234
236
  }
235
237
  // Continue with existing linting logic
@@ -299,16 +301,16 @@ class ComponentLinter {
299
301
  }
300
302
  console.log('');
301
303
  }
302
- // Generate fix suggestions
303
- const suggestions = this.generateFixSuggestions(uniqueViolations);
304
+ // Add suggestions directly to violations
305
+ this.addSuggestionsToViolations(uniqueViolations);
304
306
  return {
305
307
  success: criticalCount === 0 && highCount === 0, // Only fail on critical/high
306
308
  violations: uniqueViolations,
307
- suggestions,
308
309
  criticalCount,
309
310
  highCount,
310
311
  mediumCount,
311
- lowCount
312
+ lowCount,
313
+ hasErrors: criticalCount > 0 || highCount > 0
312
314
  };
313
315
  }
314
316
  catch (error) {
@@ -322,7 +324,7 @@ class ComponentLinter {
322
324
  column: 0,
323
325
  message: `Failed to parse component: ${error instanceof Error ? error.message : 'Unknown error'}`
324
326
  }],
325
- suggestions: []
327
+ hasErrors: true
326
328
  };
327
329
  }
328
330
  }
@@ -690,14 +692,17 @@ class ComponentLinter {
690
692
  });
691
693
  return unique;
692
694
  }
693
- static generateFixSuggestions(violations) {
694
- const suggestions = [];
695
+ /**
696
+ * Adds suggestions directly to violations based on their rule type
697
+ * @param violations Array of violations to enhance with suggestions
698
+ * @returns The same violations array with suggestions embedded
699
+ */
700
+ static addSuggestionsToViolations(violations) {
695
701
  for (const violation of violations) {
696
702
  switch (violation.rule) {
697
703
  case 'no-import-statements':
698
- suggestions.push({
699
- violation: violation.rule,
700
- suggestion: 'Remove all import statements. Interactive components receive everything through props.',
704
+ violation.suggestion = {
705
+ text: 'Remove all import statements. Interactive components receive everything through props.',
701
706
  example: `// ❌ WRONG - Using import statements:
702
707
  import React from 'react';
703
708
  import { useState } from 'react';
@@ -725,12 +730,11 @@ function MyComponent({ utilities, styles, components }) {
725
730
  // 2. Passed through the 'components' prop (child components)
726
731
  // 3. Passed through the 'styles' prop (styling)
727
732
  // 4. Available globally (React hooks)`
728
- });
733
+ };
729
734
  break;
730
735
  case 'no-export-statements':
731
- suggestions.push({
732
- violation: violation.rule,
733
- suggestion: 'Remove all export statements. The component function should be the only code, not exported.',
736
+ violation.suggestion = {
737
+ text: 'Remove all export statements. The component function should be the only code, not exported.',
734
738
  example: `// ❌ WRONG - Using export:
735
739
  export function MyComponent({ utilities }) {
736
740
  return <div>Hello</div>;
@@ -752,12 +756,11 @@ function MyComponent({ utilities, styles, components }) {
752
756
  // The component is self-contained.
753
757
  // No exports needed - the host environment
754
758
  // will execute the function directly.`
755
- });
759
+ };
756
760
  break;
757
761
  case 'no-require-statements':
758
- suggestions.push({
759
- violation: violation.rule,
760
- suggestion: 'Remove all require() and dynamic import() statements. Use props instead.',
762
+ violation.suggestion = {
763
+ text: 'Remove all require() and dynamic import() statements. Use props instead.',
761
764
  example: `// ❌ WRONG - Using require or dynamic import:
762
765
  function MyComponent({ utilities }) {
763
766
  const lodash = require('lodash');
@@ -788,12 +791,11 @@ function MyComponent({ utilities, styles, components }) {
788
791
  // - Passed via props (utilities, components, styles)
789
792
  // - Available globally (React hooks)
790
793
  // No module loading allowed!`
791
- });
794
+ };
792
795
  break;
793
796
  case 'use-function-declaration':
794
- suggestions.push({
795
- violation: violation.rule,
796
- suggestion: 'Use function declaration syntax for TOP-LEVEL component definitions. Arrow functions are fine inside components.',
797
+ violation.suggestion = {
798
+ text: 'Use function declaration syntax for TOP-LEVEL component definitions. Arrow functions are fine inside components.',
797
799
  example: `// ❌ WRONG - Top-level arrow function component:
798
800
  const MyComponent = ({ utilities, styles, components }) => {
799
801
  const [state, setState] = useState('');
@@ -826,12 +828,11 @@ function ChildComponent() {
826
828
  // 3. Hoisting allows flexible code organization
827
829
  // 4. Consistent with React documentation patterns
828
830
  // 5. Easier to distinguish from regular variables`
829
- });
831
+ };
830
832
  break;
831
833
  case 'no-return-component':
832
- suggestions.push({
833
- violation: violation.rule,
834
- suggestion: 'Remove the return statement at the end of the file. The component function should stand alone.',
834
+ violation.suggestion = {
835
+ text: 'Remove the return statement at the end of the file. The component function should stand alone.',
835
836
  example: `// ❌ WRONG - Returning the component:
836
837
  function MyComponent({ utilities, styles, components }) {
837
838
  const [state, setState] = useState('');
@@ -858,12 +859,11 @@ function MyComponent({ utilities, styles, components }) {
858
859
 
859
860
  // The runtime will find and execute your component
860
861
  // by its function name. No need to return or reference it!`
861
- });
862
+ };
862
863
  break;
863
864
  case 'no-iife-wrapper':
864
- suggestions.push({
865
- violation: violation.rule,
866
- suggestion: 'Remove the IIFE wrapper. Component code should be plain functions, not wrapped in immediately invoked functions.',
865
+ violation.suggestion = {
866
+ text: 'Remove the IIFE wrapper. Component code should be plain functions, not wrapped in immediately invoked functions.',
867
867
  example: `// ❌ WRONG - IIFE wrapper patterns:
868
868
  (function() {
869
869
  function MyComponent({ utilities, styles, components }) {
@@ -897,12 +897,11 @@ function MyComponent({ utilities, styles, components }) {
897
897
  // 3. IIFEs prevent proper component discovery
898
898
  // 4. Makes debugging harder
899
899
  // 5. Unnecessary complexity`
900
- });
900
+ };
901
901
  break;
902
902
  case 'full-state-ownership':
903
- suggestions.push({
904
- violation: violation.rule,
905
- suggestion: 'Components must manage ALL their own state internally. Use proper naming conventions for initialization.',
903
+ violation.suggestion = {
904
+ text: 'Components must manage ALL their own state internally. Use proper naming conventions for initialization.',
906
905
  example: `// ❌ WRONG - Controlled state props:
907
906
  function PaginationControls({ currentPage, filters, sortBy, onPageChange }) {
908
907
  // These props suggest parent controls the state - WRONG!
@@ -962,12 +961,11 @@ function DataTable({
962
961
  // - Direct state names (currentPage, selectedId, activeTab)
963
962
  // - State without 'initial'/'default' prefix (sortBy, filters, searchTerm)
964
963
  // - Controlled patterns (value + onChange, checked + onChange)`
965
- });
964
+ };
966
965
  break;
967
966
  case 'no-use-reducer':
968
- suggestions.push({
969
- violation: violation.rule,
970
- suggestion: 'Use useState for state management, not useReducer',
967
+ violation.suggestion = {
968
+ text: 'Use useState for state management, not useReducer',
971
969
  example: `// Instead of:
972
970
  const [state, dispatch] = useReducer(reducer, initialState);
973
971
 
@@ -990,12 +988,11 @@ function Component({ savedUserSettings, onSaveUserSettings }) {
990
988
  }
991
989
  };
992
990
  }`
993
- });
991
+ };
994
992
  break;
995
993
  case 'no-data-prop':
996
- suggestions.push({
997
- violation: violation.rule,
998
- suggestion: 'Replace generic data prop with specific named props',
994
+ violation.suggestion = {
995
+ text: 'Replace generic data prop with specific named props',
999
996
  example: `// Instead of:
1000
997
  function Component({ data, savedUserSettings, onSaveUserSettings }) {
1001
998
  return <div>{data.items.map(...)}</div>;
@@ -1013,12 +1010,11 @@ function Component({ items, customers, savedUserSettings, onSaveUserSettings })
1013
1010
 
1014
1011
  // Load data using utilities:
1015
1012
  const result = await utilities.rv.RunView({ entityName: 'Items' });`
1016
- });
1013
+ };
1017
1014
  break;
1018
1015
  case 'saved-user-settings-pattern':
1019
- suggestions.push({
1020
- violation: violation.rule,
1021
- suggestion: 'Only save important user preferences, not ephemeral UI state',
1016
+ violation.suggestion = {
1017
+ text: 'Only save important user preferences, not ephemeral UI state',
1022
1018
  example: `// ✅ SAVE these (important preferences):
1023
1019
  - Selected items/tabs: selectedCustomerId, activeTab
1024
1020
  - Sort preferences: sortBy, sortDirection
@@ -1043,12 +1039,11 @@ const handleSelect = (id) => {
1043
1039
  selectedId: id
1044
1040
  });
1045
1041
  };`
1046
- });
1042
+ };
1047
1043
  break;
1048
1044
  case 'pass-standard-props':
1049
- suggestions.push({
1050
- violation: violation.rule,
1051
- suggestion: 'Always pass standard props to all components',
1045
+ violation.suggestion = {
1046
+ text: 'Always pass standard props to all components',
1052
1047
  example: `// Always include these props when calling components:
1053
1048
  <ChildComponent
1054
1049
  items={items} // Data props
@@ -1063,19 +1058,17 @@ const handleSelect = (id) => {
1063
1058
  components={components}
1064
1059
  callbacks={callbacks}
1065
1060
  />`
1066
- });
1061
+ };
1067
1062
  break;
1068
1063
  case 'no-child-implementation':
1069
- suggestions.push({
1070
- violation: violation.rule,
1071
- suggestion: 'Remove child component implementations. Only the root component function should be in this file',
1064
+ violation.suggestion = {
1065
+ text: 'Remove child component implementations. Only the root component function should be in this file',
1072
1066
  example: 'Move child component functions to separate generation requests'
1073
- });
1067
+ };
1074
1068
  break;
1075
1069
  case 'undefined-component-usage':
1076
- suggestions.push({
1077
- violation: violation.rule,
1078
- suggestion: 'Ensure all components destructured from the components prop are defined in the component spec dependencies',
1070
+ violation.suggestion = {
1071
+ text: 'Ensure all components destructured from the components prop are defined in the component spec dependencies',
1079
1072
  example: `// Component spec should include all referenced components:
1080
1073
  {
1081
1074
  "name": "MyComponent",
@@ -1100,12 +1093,11 @@ const handleSelect = (id) => {
1100
1093
  // Then in your component:
1101
1094
  const { ModelTreeView, PromptTable, FilterPanel } = components;
1102
1095
  // All these will be available`
1103
- });
1096
+ };
1104
1097
  break;
1105
1098
  case 'component-usage-without-destructuring':
1106
- suggestions.push({
1107
- violation: violation.rule,
1108
- suggestion: 'Components must be properly accessed - either destructure from components prop or use dot notation',
1099
+ violation.suggestion = {
1100
+ text: 'Components must be properly accessed - either destructure from components prop or use dot notation',
1109
1101
  example: `// ❌ WRONG - Using component without destructuring:
1110
1102
  function MyComponent({ components }) {
1111
1103
  return <AccountList />; // Error: AccountList not destructured
@@ -1126,12 +1118,11 @@ function MyComponent({ components }) {
1126
1118
  function MyComponent({ components: { AccountList } }) {
1127
1119
  return <AccountList />;
1128
1120
  }`
1129
- });
1121
+ };
1130
1122
  break;
1131
1123
  case 'unsafe-array-access':
1132
- suggestions.push({
1133
- violation: violation.rule,
1134
- suggestion: 'Always check array bounds before accessing elements',
1124
+ violation.suggestion = {
1125
+ text: 'Always check array bounds before accessing elements',
1135
1126
  example: `// ❌ UNSAFE:
1136
1127
  const firstItem = items[0].name;
1137
1128
  const total = data[0].reduce((sum, item) => sum + item.value, 0);
@@ -1145,12 +1136,11 @@ const total = data.length > 0
1145
1136
  // ✅ BETTER - Use optional chaining:
1146
1137
  const firstItem = items[0]?.name || 'No items';
1147
1138
  const total = data[0]?.reduce((sum, item) => sum + item.value, 0) || 0;`
1148
- });
1139
+ };
1149
1140
  break;
1150
1141
  case 'array-reduce-safety':
1151
- suggestions.push({
1152
- violation: violation.rule,
1153
- suggestion: 'Always provide an initial value for reduce() or check array length',
1142
+ violation.suggestion = {
1143
+ text: 'Always provide an initial value for reduce() or check array length',
1154
1144
  example: `// ❌ UNSAFE:
1155
1145
  const sum = numbers.reduce((a, b) => a + b); // Fails on empty array
1156
1146
  const total = data[0].reduce((sum, item) => sum + item.value); // Multiple issues
@@ -1165,12 +1155,11 @@ const total = data.length > 0 && data[0]
1165
1155
  const sum = numbers.length > 0
1166
1156
  ? numbers.reduce((a, b) => a + b)
1167
1157
  : 0;`
1168
- });
1158
+ };
1169
1159
  break;
1170
1160
  case 'entity-name-mismatch':
1171
- suggestions.push({
1172
- violation: violation.rule,
1173
- suggestion: 'Use the exact entity name from dataRequirements in RunView calls',
1161
+ violation.suggestion = {
1162
+ text: 'Use the exact entity name from dataRequirements in RunView calls',
1174
1163
  example: `// The component spec defines the entities to use:
1175
1164
  // dataRequirements: {
1176
1165
  // entities: [
@@ -1198,12 +1187,11 @@ await utilities.rv.RunViews([
1198
1187
 
1199
1188
  // The linter validates that all entity names in RunView/RunViews calls
1200
1189
  // match those declared in the component spec's dataRequirements`
1201
- });
1190
+ };
1202
1191
  break;
1203
1192
  case 'missing-query-parameter':
1204
- suggestions.push({
1205
- violation: violation.rule,
1206
- suggestion: 'Provide all required parameters defined in dataRequirements for the query',
1193
+ violation.suggestion = {
1194
+ text: 'Provide all required parameters defined in dataRequirements for the query',
1207
1195
  example: `// The component spec defines required parameters:
1208
1196
  // dataRequirements: {
1209
1197
  // queries: [
@@ -1234,12 +1222,11 @@ await utilities.rq.RunQuery({
1234
1222
  StartDate: startDate // All parameters included
1235
1223
  }
1236
1224
  });`
1237
- });
1225
+ };
1238
1226
  break;
1239
1227
  case 'unknown-query-parameter':
1240
- suggestions.push({
1241
- violation: violation.rule,
1242
- suggestion: 'Only use parameters that are defined in dataRequirements for the query',
1228
+ violation.suggestion = {
1229
+ text: 'Only use parameters that are defined in dataRequirements for the query',
1243
1230
  example: `// ❌ WRONG - Using undefined parameter:
1244
1231
  await utilities.rq.RunQuery({
1245
1232
  QueryName: "User Activity Summary",
@@ -1258,12 +1245,11 @@ await utilities.rq.RunQuery({
1258
1245
  StartDate: startDate // Only parameters from dataRequirements
1259
1246
  }
1260
1247
  });`
1261
- });
1248
+ };
1262
1249
  break;
1263
1250
  case 'missing-parameters-object':
1264
- suggestions.push({
1265
- violation: violation.rule,
1266
- suggestion: 'Queries with parameters must include a Parameters object in RunQuery',
1251
+ violation.suggestion = {
1252
+ text: 'Queries with parameters must include a Parameters object in RunQuery',
1267
1253
  example: `// ❌ WRONG - Query requires parameters but none provided:
1268
1254
  await utilities.rq.RunQuery({
1269
1255
  QueryName: "User Activity Summary"
@@ -1278,12 +1264,11 @@ await utilities.rq.RunQuery({
1278
1264
  StartDate: startDate
1279
1265
  }
1280
1266
  });`
1281
- });
1267
+ };
1282
1268
  break;
1283
1269
  case 'query-name-mismatch':
1284
- suggestions.push({
1285
- violation: violation.rule,
1286
- suggestion: 'Use the exact query name from dataRequirements in RunQuery calls',
1270
+ violation.suggestion = {
1271
+ text: 'Use the exact query name from dataRequirements in RunQuery calls',
1287
1272
  example: `// The component spec defines the queries to use:
1288
1273
  // dataRequirements: {
1289
1274
  // queries: [
@@ -1305,12 +1290,11 @@ await utilities.rv.RunQuery({
1305
1290
 
1306
1291
  // The linter validates that all query names in RunQuery calls
1307
1292
  // match those declared in the component spec's dataRequirements.queries`
1308
- });
1293
+ };
1309
1294
  break;
1310
1295
  case 'runview-sql-function':
1311
- suggestions.push({
1312
- violation: violation.rule,
1313
- suggestion: 'RunView does not support SQL aggregations. Use RunQuery or aggregate in JavaScript.',
1296
+ violation.suggestion = {
1297
+ text: 'RunView does not support SQL aggregations. Use RunQuery or aggregate in JavaScript.',
1314
1298
  example: `// ❌ WRONG - SQL functions in RunView:
1315
1299
  await utilities.rv.RunView({
1316
1300
  EntityName: 'Accounts',
@@ -1332,12 +1316,11 @@ if (result?.Success) {
1332
1316
  const total = result.Results.length;
1333
1317
  const totalRevenue = result.Results.reduce((sum, acc) => sum + (acc.Revenue || 0), 0);
1334
1318
  }`
1335
- });
1319
+ };
1336
1320
  break;
1337
1321
  case 'field-not-in-requirements':
1338
- suggestions.push({
1339
- violation: violation.rule,
1340
- suggestion: 'Only use fields that are defined in dataRequirements for the entity',
1322
+ violation.suggestion = {
1323
+ text: 'Only use fields that are defined in dataRequirements for the entity',
1341
1324
  example: `// Check your dataRequirements to see allowed fields:
1342
1325
  // dataRequirements: {
1343
1326
  // entities: [{
@@ -1359,12 +1342,11 @@ await utilities.rv.RunView({
1359
1342
  EntityName: 'Accounts',
1360
1343
  Fields: ['ID', 'AccountName', 'Industry'] // All from displayFields
1361
1344
  });`
1362
- });
1345
+ };
1363
1346
  break;
1364
1347
  case 'orderby-field-not-sortable':
1365
- suggestions.push({
1366
- violation: violation.rule,
1367
- suggestion: 'OrderBy fields must be in the sortFields array for the entity',
1348
+ violation.suggestion = {
1349
+ text: 'OrderBy fields must be in the sortFields array for the entity',
1368
1350
  example: `// ❌ WRONG - Sorting by non-sortable field:
1369
1351
  await utilities.rv.RunView({
1370
1352
  EntityName: 'Accounts',
@@ -1376,12 +1358,11 @@ await utilities.rv.RunView({
1376
1358
  EntityName: 'Accounts',
1377
1359
  OrderBy: 'AccountName ASC' // AccountName is in sortFields
1378
1360
  });`
1379
- });
1361
+ };
1380
1362
  break;
1381
1363
  case 'parent-event-callback-usage':
1382
- suggestions.push({
1383
- violation: violation.rule,
1384
- suggestion: 'Components must invoke parent event callbacks when state changes',
1364
+ violation.suggestion = {
1365
+ text: 'Components must invoke parent event callbacks when state changes',
1385
1366
  example: `// ❌ WRONG - Only updating internal state:
1386
1367
  function ChildComponent({ onSelectAccount, savedUserSettings, onSaveUserSettings }) {
1387
1368
  const [selectedAccountId, setSelectedAccountId] = useState(savedUserSettings?.selectedAccountId);
@@ -1410,12 +1391,11 @@ function ChildComponent({ onSelectAccount, savedUserSettings, onSaveUserSettings
1410
1391
  onSaveUserSettings?.({ ...savedUserSettings, selectedAccountId: accountId });
1411
1392
  };
1412
1393
  }`
1413
- });
1394
+ };
1414
1395
  break;
1415
1396
  case 'property-name-consistency':
1416
- suggestions.push({
1417
- violation: violation.rule,
1418
- suggestion: 'Maintain consistent property names when transforming data',
1397
+ violation.suggestion = {
1398
+ text: 'Maintain consistent property names when transforming data',
1419
1399
  example: `// ❌ WRONG - Transform to camelCase but access as PascalCase:
1420
1400
  setAccountData(results.map(item => ({
1421
1401
  accountName: item.AccountName, // camelCase
@@ -1442,12 +1422,11 @@ setAccountData(results.map(item => ({
1442
1422
  // Later in render...
1443
1423
  <td>{account.accountName}</td> // Use camelCase consistently
1444
1424
  <td>{formatCurrency(account.annualRevenue)}</td> // Works!`
1445
- });
1425
+ };
1446
1426
  break;
1447
1427
  case 'noisy-settings-updates':
1448
- suggestions.push({
1449
- violation: violation.rule,
1450
- suggestion: 'Save settings sparingly - only on meaningful user actions',
1428
+ violation.suggestion = {
1429
+ text: 'Save settings sparingly - only on meaningful user actions',
1451
1430
  example: `// ❌ WRONG - Saving on every keystroke:
1452
1431
  const handleSearchChange = (e) => {
1453
1432
  setSearchTerm(e.target.value);
@@ -1468,12 +1447,11 @@ const saveSearchTerm = useMemo(() =>
1468
1447
  }, 500),
1469
1448
  [savedUserSettings]
1470
1449
  );`
1471
- });
1450
+ };
1472
1451
  break;
1473
1452
  case 'prop-state-sync':
1474
- suggestions.push({
1475
- violation: violation.rule,
1476
- suggestion: 'Initialize state once, don\'t sync from props',
1453
+ violation.suggestion = {
1454
+ text: 'Initialize state once, don\'t sync from props',
1477
1455
  example: `// ❌ WRONG - Syncing prop to state:
1478
1456
  const [value, setValue] = useState(propValue);
1479
1457
  useEffect(() => {
@@ -1487,12 +1465,11 @@ const [value, setValue] = useState(
1487
1465
 
1488
1466
  // ✅ CORRECT - If you need prop changes, use derived state:
1489
1467
  const displayValue = propOverride || value;`
1490
- });
1468
+ };
1491
1469
  break;
1492
1470
  case 'performance-memoization':
1493
- suggestions.push({
1494
- violation: violation.rule,
1495
- suggestion: 'Use useMemo for expensive operations and static data',
1471
+ violation.suggestion = {
1472
+ text: 'Use useMemo for expensive operations and static data',
1496
1473
  example: `// ❌ WRONG - Expensive operation on every render:
1497
1474
  const filteredItems = items.filter(item =>
1498
1475
  item.name.toLowerCase().includes(searchTerm.toLowerCase())
@@ -1517,12 +1494,11 @@ const columns = useMemo(() => [
1517
1494
  { field: 'name', header: 'Name' },
1518
1495
  { field: 'value', header: 'Value' }
1519
1496
  ], []); // Empty deps = never changes`
1520
- });
1497
+ };
1521
1498
  break;
1522
1499
  case 'child-state-management':
1523
- suggestions.push({
1524
- violation: violation.rule,
1525
- suggestion: 'Never manage state for child components',
1500
+ violation.suggestion = {
1501
+ text: 'Never manage state for child components',
1526
1502
  example: `// ❌ WRONG - Managing child state:
1527
1503
  const [childTableSort, setChildTableSort] = useState('name');
1528
1504
  const [modalOpen, setModalOpen] = useState(false);
@@ -1539,12 +1515,11 @@ const [modalOpen, setModalOpen] = useState(false);
1539
1515
  onSaveUserSettings={handleChildSettings}
1540
1516
  // Child manages its own sort state!
1541
1517
  />`
1542
- });
1518
+ };
1543
1519
  break;
1544
1520
  case 'server-reload-on-client-operation':
1545
- suggestions.push({
1546
- violation: violation.rule,
1547
- suggestion: 'Use client-side operations for sorting and filtering',
1521
+ violation.suggestion = {
1522
+ text: 'Use client-side operations for sorting and filtering',
1548
1523
  example: `// ❌ WRONG - Reload from server:
1549
1524
  const handleSort = (field) => {
1550
1525
  setSortBy(field);
@@ -1568,12 +1543,11 @@ const sortedData = useMemo(() => {
1568
1543
  });
1569
1544
  return sorted;
1570
1545
  }, [data, sortBy, sortDirection]);`
1571
- });
1546
+ };
1572
1547
  break;
1573
1548
  case 'runview-runquery-valid-properties':
1574
- suggestions.push({
1575
- violation: violation.rule,
1576
- suggestion: 'Use only valid properties for RunView/RunViews and RunQuery',
1549
+ violation.suggestion = {
1550
+ text: 'Use only valid properties for RunView/RunViews and RunQuery',
1577
1551
  example: `// ❌ WRONG - Invalid properties on RunView:
1578
1552
  await utilities.rv.RunView({
1579
1553
  EntityName: 'MJ: AI Prompt Runs',
@@ -1602,23 +1576,34 @@ await utilities.rq.RunQuery({
1602
1576
  // Valid RunQuery properties:
1603
1577
  // - QueryName (required)
1604
1578
  // - CategoryName, CategoryID, Parameters (optional)`
1605
- });
1579
+ };
1606
1580
  break;
1607
1581
  case 'component-props-validation':
1608
- suggestions.push({
1609
- violation: violation.rule,
1610
- suggestion: 'Components can only accept standard props or props defined in spec. Load data internally.',
1611
- example: `// WRONG - Root component with additional props:
1612
- function RootComponent({ utilities, styles, components, customers, orders, selectedId }) {
1613
- // Additional props will break hosting environment
1582
+ violation.suggestion = {
1583
+ text: 'Components can only accept standard props and props explicitly defined in the component spec. Additional props must be declared in the spec\'s properties array.',
1584
+ example: `// WRONG - Component with undeclared props:
1585
+ function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
1586
+ // customers, orders, selectedId are NOT allowed unless defined in spec
1614
1587
  }
1615
1588
 
1616
- // ✅ CORRECT - Root component with only standard props:
1617
- function RootComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
1618
- // Load ALL data internally using utilities
1589
+ // ✅ CORRECT Option 1 - Use only standard props and load data internally:
1590
+ function MyComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
1591
+ // Load data internally using utilities
1619
1592
  const [customers, setCustomers] = useState([]);
1620
1593
  const [orders, setOrders] = useState([]);
1621
1594
  const [selectedId, setSelectedId] = useState(savedUserSettings?.selectedId);
1595
+ }
1596
+
1597
+ // ✅ CORRECT Option 2 - Define props in component spec:
1598
+ // In spec.properties array:
1599
+ // [
1600
+ // { name: "customers", type: "array", required: false, description: "Customer list" },
1601
+ // { name: "orders", type: "array", required: false, description: "Order list" },
1602
+ // { name: "selectedId", type: "string", required: false, description: "Selected item ID" }
1603
+ // ]
1604
+ // Then the component can accept them:
1605
+ function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
1606
+ // These props are now allowed because they're defined in the spec
1622
1607
 
1623
1608
  useEffect(() => {
1624
1609
  const loadData = async () => {
@@ -1639,12 +1624,11 @@ function RootComponent({ utilities, styles, components, callbacks, savedUserSett
1639
1624
 
1640
1625
  return <div>{/* Use state, not props */}</div>;
1641
1626
  }`
1642
- });
1627
+ };
1643
1628
  break;
1644
1629
  case 'runview-runquery-result-direct-usage':
1645
- suggestions.push({
1646
- violation: violation.rule,
1647
- suggestion: 'RunView and RunQuery return result objects, not arrays. Access the data with .Results property.',
1630
+ violation.suggestion = {
1631
+ text: 'RunView and RunQuery return result objects, not arrays. Access the data with .Results property.',
1648
1632
  example: `// ❌ WRONG - Using result directly as array:
1649
1633
  const result = await utilities.rv.RunView({
1650
1634
  EntityName: 'Users',
@@ -1690,12 +1674,11 @@ setData(queryResult.Results || []); // NOT queryResult directly!
1690
1674
  // TotalRowCount?: number,
1691
1675
  // ExecutionTime?: number
1692
1676
  // }`
1693
- });
1677
+ };
1694
1678
  break;
1695
1679
  case 'styles-invalid-path':
1696
- suggestions.push({
1697
- violation: violation.rule,
1698
- suggestion: 'Fix invalid styles property paths. Use the correct ComponentStyles interface structure.',
1680
+ violation.suggestion = {
1681
+ text: 'Fix invalid styles property paths. Use the correct ComponentStyles interface structure.',
1699
1682
  example: `// ❌ WRONG - Invalid property paths:
1700
1683
  styles.fontSize.small // fontSize is not at root level
1701
1684
  styles.colors.background // colors.background exists
@@ -1710,12 +1693,11 @@ styles.spacing.sm // correct size name
1710
1693
  styles?.typography?.fontSize?.sm || '14px'
1711
1694
  styles?.colors?.background || '#FFFFFF'
1712
1695
  styles?.spacing?.sm || '8px'`
1713
- });
1696
+ };
1714
1697
  break;
1715
1698
  case 'styles-unsafe-access':
1716
- suggestions.push({
1717
- violation: violation.rule,
1718
- suggestion: 'Use optional chaining for nested styles access to prevent runtime errors.',
1699
+ violation.suggestion = {
1700
+ text: 'Use optional chaining for nested styles access to prevent runtime errors.',
1719
1701
  example: `// ❌ UNSAFE - Direct nested access:
1720
1702
  const fontSize = styles.typography.fontSize.md;
1721
1703
  const borderRadius = styles.borders.radius.sm;
@@ -1730,45 +1712,39 @@ const {
1730
1712
  fontSize: { md: fontSize = '14px' } = {}
1731
1713
  } = {}
1732
1714
  } = styles || {};`
1733
- });
1715
+ };
1734
1716
  break;
1735
1717
  }
1736
1718
  }
1737
- return suggestions;
1719
+ return violations;
1738
1720
  }
1739
1721
  static generateSyntaxErrorSuggestions(violations) {
1740
- const suggestions = [];
1741
1722
  for (const violation of violations) {
1742
1723
  if (violation.message.includes('Unterminated string')) {
1743
- suggestions.push({
1744
- violation: violation.rule,
1745
- suggestion: 'Check that all string literals are properly closed with matching quotes',
1724
+ violation.suggestion = {
1725
+ text: 'Check that all string literals are properly closed with matching quotes',
1746
1726
  example: 'Template literals with interpolation must use backticks: `text ${variable} text`'
1747
- });
1727
+ };
1748
1728
  }
1749
1729
  else if (violation.message.includes('Unexpected token') || violation.message.includes('export')) {
1750
- suggestions.push({
1751
- violation: violation.rule,
1752
- suggestion: 'Ensure all code is within the component function body',
1730
+ violation.suggestion = {
1731
+ text: 'Ensure all code is within the component function body',
1753
1732
  example: 'Remove any export statements or code outside the function definition'
1754
- });
1733
+ };
1755
1734
  }
1756
1735
  else if (violation.message.includes('import') && violation.message.includes('top level')) {
1757
- suggestions.push({
1758
- violation: violation.rule,
1759
- suggestion: 'Import statements are not allowed in components - use props instead',
1736
+ violation.suggestion = {
1737
+ text: 'Import statements are not allowed in components - use props instead',
1760
1738
  example: 'Access libraries through props: const { React, MaterialUI } = props.components'
1761
- });
1739
+ };
1762
1740
  }
1763
1741
  else {
1764
- suggestions.push({
1765
- violation: violation.rule,
1766
- suggestion: 'Fix the syntax error before the component can be compiled',
1742
+ violation.suggestion = {
1743
+ text: 'Fix the syntax error before the component can be compiled',
1767
1744
  example: 'Review the code at the specified line and column for syntax issues'
1768
- });
1745
+ };
1769
1746
  }
1770
1747
  }
1771
- return suggestions;
1772
1748
  }
1773
1749
  /**
1774
1750
  * Apply library-specific lint rules based on ComponentLibrary LintRules field
@@ -2513,7 +2489,7 @@ ComponentLinter.universalComponentRules = [
2513
2489
  severity: 'critical',
2514
2490
  line: path.node.loc?.start.line || 0,
2515
2491
  column: path.node.loc?.start.column || 0,
2516
- message: `Component '${varName}' shadows a dependency component. The component '${varName}' is already available from dependencies (auto-destructured), but this code is creating a new definition which overrides it.`,
2492
+ message: `Component '${varName}' shadows a dependency component. The component '${varName}' should be accessed via destructuring from components prop or as components.${varName}, but this code is creating a new definition which overrides it.`,
2517
2493
  code: `const ${varName} = ...`
2518
2494
  });
2519
2495
  }
@@ -2531,15 +2507,15 @@ ComponentLinter.universalComponentRules = [
2531
2507
  severity: 'critical',
2532
2508
  line: path.node.loc?.start.line || 0,
2533
2509
  column: path.node.loc?.start.column || 0,
2534
- message: `Component '${funcName}' shadows a dependency component. The component '${funcName}' is already available from dependencies (auto-destructured), but this code is creating a new function which overrides it.`,
2510
+ message: `Component '${funcName}' shadows a dependency component. The component '${funcName}' should be accessed via destructuring from components prop or as components.${funcName}, but this code is creating a new function which overrides it.`,
2535
2511
  code: `function ${funcName}(...)`
2536
2512
  });
2537
2513
  }
2538
2514
  }
2539
2515
  }
2540
2516
  });
2541
- // Components are now auto-destructured in the wrapper, so we don't need to check for manual destructuring
2542
- // We just need to check if they're being used directly
2517
+ // Components must be destructured from the components prop or accessed via components.ComponentName
2518
+ // Check if they're being used correctly
2543
2519
  let hasComponentsUsage = false;
2544
2520
  const usedDependencies = new Set();
2545
2521
  mainComponentPath.traverse({
@@ -2580,7 +2556,7 @@ ComponentLinter.universalComponentRules = [
2580
2556
  }
2581
2557
  }
2582
2558
  });
2583
- // Components are now auto-destructured, so just check for unused dependencies
2559
+ // Check for unused dependencies - components must be destructured or accessed via components prop
2584
2560
  if (dependencyNames.size > 0 && usedDependencies.size === 0) {
2585
2561
  const depList = Array.from(dependencyNames).join(', ');
2586
2562
  violations.push({
@@ -2588,7 +2564,7 @@ ComponentLinter.universalComponentRules = [
2588
2564
  severity: 'low',
2589
2565
  line: mainComponentPath.node.loc?.start.line || 0,
2590
2566
  column: mainComponentPath.node.loc?.start.column || 0,
2591
- message: `Component has dependencies [${depList}] defined in spec but they're not being used. These components are available for use.`,
2567
+ message: `Component has dependencies [${depList}] defined in spec but they're not being used. These components must be destructured from the components prop or accessed as components.ComponentName to use them.`,
2592
2568
  code: `// Available: ${depList}`
2593
2569
  });
2594
2570
  }
@@ -4954,10 +4930,13 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4954
4930
  // Build set of allowed props: standard props + React special props + componentSpec properties
4955
4931
  const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
4956
4932
  // Add props from componentSpec.properties if they exist
4933
+ // These are the architect-defined props that this component is allowed to accept
4934
+ const specDefinedProps = [];
4957
4935
  if (componentSpec?.properties) {
4958
4936
  for (const prop of componentSpec.properties) {
4959
4937
  if (prop.name) {
4960
4938
  allowedProps.add(prop.name);
4939
+ specDefinedProps.push(prop.name);
4961
4940
  }
4962
4941
  }
4963
4942
  }
@@ -4979,15 +4958,28 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4979
4958
  }
4980
4959
  // Only report if there are non-allowed props
4981
4960
  if (invalidProps.length > 0) {
4982
- const customPropsMessage = componentSpec?.properties?.length
4983
- ? ` and custom props defined in spec: ${componentSpec.properties.map(p => p.name).join(', ')}`
4984
- : '';
4961
+ let message;
4962
+ if (specDefinedProps.length > 0) {
4963
+ message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
4964
+ `This component can only accept: ` +
4965
+ `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
4966
+ `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
4967
+ `(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
4968
+ `Any additional props must be defined in the component spec's properties array.`;
4969
+ }
4970
+ else {
4971
+ message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
4972
+ `This component can only accept: ` +
4973
+ `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
4974
+ `(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
4975
+ `To accept additional props, they must be defined in the component spec's properties array.`;
4976
+ }
4985
4977
  violations.push({
4986
4978
  rule: 'component-props-validation',
4987
4979
  severity: 'critical',
4988
4980
  line: path.node.loc?.start.line || 0,
4989
4981
  column: path.node.loc?.start.column || 0,
4990
- message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}, React special props: ${Array.from(reactSpecialProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
4982
+ message
4991
4983
  });
4992
4984
  }
4993
4985
  }
@@ -5012,15 +5004,28 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5012
5004
  }
5013
5005
  }
5014
5006
  if (invalidProps.length > 0) {
5015
- const customPropsMessage = componentSpec?.properties?.length
5016
- ? ` and custom props defined in spec: ${componentSpec.properties.map(p => p.name).join(', ')}`
5017
- : '';
5007
+ let message;
5008
+ if (specDefinedProps.length > 0) {
5009
+ message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5010
+ `This component can only accept: ` +
5011
+ `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5012
+ `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
5013
+ `(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5014
+ `Any additional props must be defined in the component spec's properties array.`;
5015
+ }
5016
+ else {
5017
+ message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5018
+ `This component can only accept: ` +
5019
+ `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5020
+ `(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5021
+ `To accept additional props, they must be defined in the component spec's properties array.`;
5022
+ }
5018
5023
  violations.push({
5019
5024
  rule: 'component-props-validation',
5020
5025
  severity: 'critical',
5021
5026
  line: path.node.loc?.start.line || 0,
5022
5027
  column: path.node.loc?.start.column || 0,
5023
- message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}, React special props: ${Array.from(reactSpecialProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
5028
+ message
5024
5029
  });
5025
5030
  }
5026
5031
  }
@@ -5253,87 +5258,6 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5253
5258
  return violations;
5254
5259
  }
5255
5260
  },
5256
- {
5257
- name: 'invalid-components-destructuring',
5258
- appliesTo: 'all',
5259
- test: (ast, componentName, componentSpec) => {
5260
- const violations = [];
5261
- // Build sets of valid component names and library names
5262
- const validComponentNames = new Set();
5263
- const libraryNames = new Set();
5264
- const libraryGlobalVars = new Set();
5265
- // Add dependency components
5266
- if (componentSpec?.dependencies) {
5267
- for (const dep of componentSpec.dependencies) {
5268
- if (dep.name) {
5269
- validComponentNames.add(dep.name);
5270
- }
5271
- }
5272
- }
5273
- // Add libraries
5274
- if (componentSpec?.libraries) {
5275
- for (const lib of componentSpec.libraries) {
5276
- if (lib.name) {
5277
- libraryNames.add(lib.name);
5278
- }
5279
- if (lib.globalVariable) {
5280
- libraryGlobalVars.add(lib.globalVariable);
5281
- }
5282
- }
5283
- }
5284
- // Check for manual destructuring from components (now optional since auto-destructuring is in place)
5285
- (0, traverse_1.default)(ast, {
5286
- VariableDeclarator(path) {
5287
- // Look for: const { Something } = components;
5288
- if (t.isObjectPattern(path.node.id) &&
5289
- t.isIdentifier(path.node.init) &&
5290
- path.node.init.name === 'components') {
5291
- for (const prop of path.node.id.properties) {
5292
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
5293
- const destructuredName = prop.key.name;
5294
- // Check if this is NOT a valid component from dependencies
5295
- if (!validComponentNames.has(destructuredName)) {
5296
- // Check if it might be a library being incorrectly destructured
5297
- if (libraryNames.has(destructuredName) || libraryGlobalVars.has(destructuredName)) {
5298
- violations.push({
5299
- rule: 'invalid-components-destructuring',
5300
- severity: 'critical',
5301
- line: prop.loc?.start.line || 0,
5302
- column: prop.loc?.start.column || 0,
5303
- message: `Attempting to destructure library "${destructuredName}" from components prop. Libraries should be accessed directly via their globalVariable, not from components.`,
5304
- code: `const { ${destructuredName} } = components;`
5305
- });
5306
- }
5307
- else {
5308
- violations.push({
5309
- rule: 'invalid-components-destructuring',
5310
- severity: 'high',
5311
- line: prop.loc?.start.line || 0,
5312
- column: prop.loc?.start.column || 0,
5313
- 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.`,
5314
- code: `const { ${destructuredName} } = components;`
5315
- });
5316
- }
5317
- }
5318
- else {
5319
- // Valid component, but manual destructuring is now redundant
5320
- violations.push({
5321
- rule: 'invalid-components-destructuring',
5322
- severity: 'low',
5323
- line: prop.loc?.start.line || 0,
5324
- column: prop.loc?.start.column || 0,
5325
- message: `Manual destructuring of "${destructuredName}" from components prop is redundant. Components are now auto-destructured and available directly.`,
5326
- code: `const { ${destructuredName} } = components; // Can be removed`
5327
- });
5328
- }
5329
- }
5330
- }
5331
- }
5332
- }
5333
- });
5334
- return violations;
5335
- }
5336
- },
5337
5261
  {
5338
5262
  name: 'unsafe-array-operations',
5339
5263
  appliesTo: 'all',
@@ -5727,14 +5651,14 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5727
5651
  });
5728
5652
  }
5729
5653
  } else if (componentsFromProp.has(tagName)) {
5730
- // This shouldn't happen since dependency components are auto-destructured
5731
- // But keep as a fallback check
5654
+ // Component is in dependencies but not destructured/accessible
5655
+ // This indicates the component wasn't properly destructured from components prop
5732
5656
  violations.push({
5733
5657
  rule: 'undefined-jsx-component',
5734
5658
  severity: 'high',
5735
5659
  line: openingElement.loc?.start.line || 0,
5736
5660
  column: openingElement.loc?.start.column || 0,
5737
- message: `JSX component "${tagName}" is in dependencies but appears to be undefined. There may be an issue with component registration.`,
5661
+ message: `JSX component "${tagName}" is in dependencies but appears to be undefined. Make sure to destructure it from the components prop: const { ${tagName} } = components;`,
5738
5662
  code: `<${tagName} ... />`
5739
5663
  });
5740
5664
  } else {
@@ -7543,87 +7467,117 @@ Correct pattern:
7543
7467
  appliesTo: 'all',
7544
7468
  test: (ast, componentName) => {
7545
7469
  const violations = [];
7546
- // Count all function declarations and expressions at the top level
7547
- const functionDeclarations = [];
7548
- const functionExpressions = [];
7549
- (0, traverse_1.default)(ast, {
7550
- FunctionDeclaration(path) {
7551
- // Only check top-level functions (not nested inside other functions)
7552
- const parent = path.getFunctionParent();
7553
- if (!parent) {
7554
- const funcName = path.node.id?.name || 'anonymous';
7555
- functionDeclarations.push({
7556
- name: funcName,
7557
- line: path.node.loc?.start.line || 0,
7558
- column: path.node.loc?.start.column || 0
7559
- });
7560
- }
7561
- },
7562
- VariableDeclaration(path) {
7563
- // Check for const/let/var func = function() or arrow functions at top level
7564
- const parent = path.getFunctionParent();
7565
- if (!parent) {
7566
- for (const declarator of path.node.declarations) {
7567
- if (t.isVariableDeclarator(declarator) &&
7568
- (t.isFunctionExpression(declarator.init) ||
7569
- t.isArrowFunctionExpression(declarator.init))) {
7570
- const funcName = t.isIdentifier(declarator.id) ? declarator.id.name : 'anonymous';
7571
- functionExpressions.push({
7572
- name: funcName,
7573
- line: declarator.loc?.start.line || 0,
7574
- column: declarator.loc?.start.column || 0
7575
- });
7576
- }
7577
- }
7578
- }
7579
- }
7580
- });
7581
- const allFunctions = [...functionDeclarations, ...functionExpressions];
7582
- // Check if we have more than one function
7583
- if (allFunctions.length > 1) {
7584
- // Find which one is the main component
7585
- const mainComponentIndex = allFunctions.findIndex(f => f.name === componentName);
7586
- const otherFunctions = allFunctions.filter((_, index) => index !== mainComponentIndex);
7470
+ // Check that the AST body contains exactly one statement and it's a function declaration
7471
+ const programBody = ast.program.body;
7472
+ // First, check if there's anything other than a single function declaration
7473
+ if (programBody.length === 0) {
7474
+ violations.push({
7475
+ rule: 'single-function-only',
7476
+ severity: 'critical',
7477
+ line: 1,
7478
+ column: 0,
7479
+ message: `Component code must contain exactly one function declaration named "${componentName}". No code found.`,
7480
+ code: `Add: function ${componentName}({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) { ... }`
7481
+ });
7482
+ return violations;
7483
+ }
7484
+ if (programBody.length > 1) {
7485
+ // Multiple top-level statements - not allowed
7587
7486
  violations.push({
7588
7487
  rule: 'single-function-only',
7589
7488
  severity: 'critical',
7590
- line: otherFunctions[0].line,
7591
- column: otherFunctions[0].column,
7592
- message: `Component code must contain ONLY the main component function "${componentName}". Found ${allFunctions.length} functions: ${allFunctions.map(f => f.name).join(', ')}. Move other functions to separate component dependencies.`,
7593
- code: `Remove functions: ${otherFunctions.map(f => f.name).join(', ')}`
7489
+ line: programBody[1].loc?.start.line || 0,
7490
+ column: programBody[1].loc?.start.column || 0,
7491
+ message: `Component code must contain ONLY a single function declaration. Found ${programBody.length} top-level statements. No code should exist before or after the function.`,
7492
+ code: `Remove all code except: function ${componentName}(...) { ... }`
7594
7493
  });
7595
- // Add a violation for each extra function
7596
- for (const func of otherFunctions) {
7494
+ // Report each extra statement
7495
+ for (let i = 1; i < programBody.length; i++) {
7496
+ const stmt = programBody[i];
7497
+ let stmtType = 'statement';
7498
+ if (t.isVariableDeclaration(stmt)) {
7499
+ stmtType = 'variable declaration';
7500
+ }
7501
+ else if (t.isFunctionDeclaration(stmt)) {
7502
+ stmtType = 'function declaration';
7503
+ }
7504
+ else if (t.isExpressionStatement(stmt)) {
7505
+ stmtType = 'expression';
7506
+ }
7597
7507
  violations.push({
7598
7508
  rule: 'single-function-only',
7599
7509
  severity: 'critical',
7600
- line: func.line,
7601
- column: func.column,
7602
- message: `Extra function "${func.name}" not allowed. Each component must be a single function. Move this to a separate component dependency.`,
7603
- code: `function ${func.name} should be a separate component`
7510
+ line: stmt.loc?.start.line || 0,
7511
+ column: stmt.loc?.start.column || 0,
7512
+ message: `Extra ${stmtType} not allowed. Only the component function should exist.`,
7513
+ code: ''
7604
7514
  });
7605
7515
  }
7606
7516
  }
7607
- // Also check that the single function matches the component name
7608
- if (allFunctions.length === 1 && allFunctions[0].name !== componentName) {
7517
+ // Check that the single statement is a function declaration (not arrow function or other)
7518
+ const firstStatement = programBody[0];
7519
+ if (!t.isFunctionDeclaration(firstStatement)) {
7520
+ let actualType = 'unknown statement';
7521
+ let suggestion = '';
7522
+ if (t.isVariableDeclaration(firstStatement)) {
7523
+ // Check if it's an arrow function or other variable
7524
+ const declarator = firstStatement.declarations[0];
7525
+ if (t.isVariableDeclarator(declarator)) {
7526
+ if (t.isArrowFunctionExpression(declarator.init) || t.isFunctionExpression(declarator.init)) {
7527
+ actualType = 'arrow function or function expression';
7528
+ suggestion = `Use function declaration syntax: function ${componentName}(...) { ... }`;
7529
+ }
7530
+ else {
7531
+ actualType = 'variable declaration';
7532
+ suggestion = 'Remove this variable and ensure only the component function exists';
7533
+ }
7534
+ }
7535
+ }
7536
+ else if (t.isExpressionStatement(firstStatement)) {
7537
+ actualType = 'expression statement';
7538
+ suggestion = 'Remove this expression and add the component function';
7539
+ }
7609
7540
  violations.push({
7610
7541
  rule: 'single-function-only',
7611
7542
  severity: 'critical',
7612
- line: allFunctions[0].line,
7613
- column: allFunctions[0].column,
7614
- message: `Component function name "${allFunctions[0].name}" does not match component name "${componentName}". The function must be named exactly as specified.`,
7615
- code: `Rename function to: function ${componentName}(...)`
7543
+ line: firstStatement.loc?.start.line || 0,
7544
+ column: firstStatement.loc?.start.column || 0,
7545
+ message: `Component must be a function declaration, not ${actualType}. ${suggestion}`,
7546
+ code: ''
7616
7547
  });
7548
+ // Don't check name if it's not a function declaration
7549
+ return violations;
7617
7550
  }
7618
- // Check for no function at all
7619
- if (allFunctions.length === 0) {
7551
+ // Check that the function name matches the component name
7552
+ const functionName = firstStatement.id?.name;
7553
+ if (functionName !== componentName) {
7620
7554
  violations.push({
7621
7555
  rule: 'single-function-only',
7622
7556
  severity: 'critical',
7623
- line: 1,
7624
- column: 0,
7625
- message: `Component code must contain exactly one function named "${componentName}". No functions found.`,
7626
- code: `Add: function ${componentName}({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) { ... }`
7557
+ line: firstStatement.loc?.start.line || 0,
7558
+ column: firstStatement.loc?.start.column || 0,
7559
+ message: `Component function name "${functionName}" does not match component name "${componentName}". The function must be named exactly as specified.`,
7560
+ code: `Rename to: function ${componentName}(...)`
7561
+ });
7562
+ }
7563
+ // Additional check: look for any code before the function that might have been missed
7564
+ // (e.g., leading variable declarations that destructure from React)
7565
+ if (programBody.length === 1 && t.isFunctionDeclaration(firstStatement)) {
7566
+ // Use traverse to find any problematic patterns inside
7567
+ (0, traverse_1.default)(ast, {
7568
+ Program(path) {
7569
+ // Check if there are any directives or other non-obvious code
7570
+ if (path.node.directives && path.node.directives.length > 0) {
7571
+ violations.push({
7572
+ rule: 'single-function-only',
7573
+ severity: 'high',
7574
+ line: 1,
7575
+ column: 0,
7576
+ message: 'Component should not have directives like "use strict". These are added automatically.',
7577
+ code: ''
7578
+ });
7579
+ }
7580
+ }
7627
7581
  });
7628
7582
  }
7629
7583
  return violations;
@@ -8061,6 +8015,44 @@ Correct pattern:
8061
8015
  });
8062
8016
  return violations;
8063
8017
  }
8018
+ },
8019
+ {
8020
+ name: 'no-react-destructuring',
8021
+ appliesTo: 'all',
8022
+ test: (ast, componentName, componentSpec) => {
8023
+ const violations = [];
8024
+ (0, traverse_1.default)(ast, {
8025
+ VariableDeclarator(path) {
8026
+ // Check for destructuring from React
8027
+ if (t.isObjectPattern(path.node.id) &&
8028
+ t.isIdentifier(path.node.init) &&
8029
+ path.node.init.name === 'React') {
8030
+ // Get the destructured properties
8031
+ const destructuredProps = path.node.id.properties
8032
+ .filter(prop => t.isObjectProperty(prop) && t.isIdentifier(prop.key))
8033
+ .map(prop => prop.key)
8034
+ .map(key => key.name);
8035
+ violations.push({
8036
+ rule: 'no-react-destructuring',
8037
+ severity: 'critical',
8038
+ line: path.node.loc?.start.line || 0,
8039
+ column: path.node.loc?.start.column || 0,
8040
+ message: `Cannot destructure from React. The hooks (${destructuredProps.join(', ')}) are already available as global functions in the React runtime.`,
8041
+ code: path.toString().substring(0, 100),
8042
+ suggestion: {
8043
+ text: `Remove the destructuring statement. React hooks like ${destructuredProps.join(', ')} are already available globally and don't need to be imported or destructured.`,
8044
+ example: `// Remove this line entirely:
8045
+ // const { ${destructuredProps.join(', ')} } = React;
8046
+
8047
+ // Just use the hooks directly:
8048
+ const [state, setState] = useState(initialValue);`
8049
+ }
8050
+ });
8051
+ }
8052
+ }
8053
+ });
8054
+ return violations;
8055
+ }
8064
8056
  }
8065
8057
  ];
8066
8058
  //# sourceMappingURL=component-linter.js.map