@memberjunction/react-test-harness 2.95.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.
@@ -30,7 +30,9 @@ exports.ComponentLinter = void 0;
30
30
  const parser = __importStar(require("@babel/parser"));
31
31
  const traverse_1 = __importDefault(require("@babel/traverse"));
32
32
  const t = __importStar(require("@babel/types"));
33
+ const core_entities_1 = require("@memberjunction/core-entities");
33
34
  const library_lint_cache_1 = require("./library-lint-cache");
35
+ const styles_type_analyzer_1 = require("./styles-type-analyzer");
34
36
  // Standard HTML elements (lowercase)
35
37
  const HTML_ELEMENTS = new Set([
36
38
  // Main root
@@ -99,6 +101,13 @@ const runViewResultProps = [
99
101
  'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
100
102
  ];
101
103
  class ComponentLinter {
104
+ // Get or create the styles analyzer instance
105
+ static getStylesAnalyzer() {
106
+ if (!ComponentLinter.stylesAnalyzer) {
107
+ ComponentLinter.stylesAnalyzer = new styles_type_analyzer_1.StylesTypeAnalyzer();
108
+ }
109
+ return ComponentLinter.stylesAnalyzer;
110
+ }
102
111
  // Helper method to check if a statement contains a return
103
112
  static containsReturn(node) {
104
113
  let hasReturn = false;
@@ -213,14 +222,16 @@ class ComponentLinter {
213
222
  }
214
223
  // If we have critical syntax errors, return immediately with those
215
224
  if (syntaxViolations.length > 0) {
225
+ // Add suggestions directly to syntax violations
226
+ this.generateSyntaxErrorSuggestions(syntaxViolations);
216
227
  return {
217
228
  success: false,
218
229
  violations: syntaxViolations,
219
- suggestions: this.generateSyntaxErrorSuggestions(syntaxViolations),
220
230
  criticalCount: syntaxViolations.length,
221
231
  highCount: 0,
222
232
  mediumCount: 0,
223
- lowCount: 0
233
+ lowCount: 0,
234
+ hasErrors: true
224
235
  };
225
236
  }
226
237
  // Continue with existing linting logic
@@ -290,16 +301,16 @@ class ComponentLinter {
290
301
  }
291
302
  console.log('');
292
303
  }
293
- // Generate fix suggestions
294
- const suggestions = this.generateFixSuggestions(uniqueViolations);
304
+ // Add suggestions directly to violations
305
+ this.addSuggestionsToViolations(uniqueViolations);
295
306
  return {
296
307
  success: criticalCount === 0 && highCount === 0, // Only fail on critical/high
297
308
  violations: uniqueViolations,
298
- suggestions,
299
309
  criticalCount,
300
310
  highCount,
301
311
  mediumCount,
302
- lowCount
312
+ lowCount,
313
+ hasErrors: criticalCount > 0 || highCount > 0
303
314
  };
304
315
  }
305
316
  catch (error) {
@@ -313,7 +324,7 @@ class ComponentLinter {
313
324
  column: 0,
314
325
  message: `Failed to parse component: ${error instanceof Error ? error.message : 'Unknown error'}`
315
326
  }],
316
- suggestions: []
327
+ hasErrors: true
317
328
  };
318
329
  }
319
330
  }
@@ -681,14 +692,17 @@ class ComponentLinter {
681
692
  });
682
693
  return unique;
683
694
  }
684
- static generateFixSuggestions(violations) {
685
- 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) {
686
701
  for (const violation of violations) {
687
702
  switch (violation.rule) {
688
703
  case 'no-import-statements':
689
- suggestions.push({
690
- violation: violation.rule,
691
- 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.',
692
706
  example: `// ❌ WRONG - Using import statements:
693
707
  import React from 'react';
694
708
  import { useState } from 'react';
@@ -716,12 +730,11 @@ function MyComponent({ utilities, styles, components }) {
716
730
  // 2. Passed through the 'components' prop (child components)
717
731
  // 3. Passed through the 'styles' prop (styling)
718
732
  // 4. Available globally (React hooks)`
719
- });
733
+ };
720
734
  break;
721
735
  case 'no-export-statements':
722
- suggestions.push({
723
- violation: violation.rule,
724
- 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.',
725
738
  example: `// ❌ WRONG - Using export:
726
739
  export function MyComponent({ utilities }) {
727
740
  return <div>Hello</div>;
@@ -743,12 +756,11 @@ function MyComponent({ utilities, styles, components }) {
743
756
  // The component is self-contained.
744
757
  // No exports needed - the host environment
745
758
  // will execute the function directly.`
746
- });
759
+ };
747
760
  break;
748
761
  case 'no-require-statements':
749
- suggestions.push({
750
- violation: violation.rule,
751
- 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.',
752
764
  example: `// ❌ WRONG - Using require or dynamic import:
753
765
  function MyComponent({ utilities }) {
754
766
  const lodash = require('lodash');
@@ -779,12 +791,11 @@ function MyComponent({ utilities, styles, components }) {
779
791
  // - Passed via props (utilities, components, styles)
780
792
  // - Available globally (React hooks)
781
793
  // No module loading allowed!`
782
- });
794
+ };
783
795
  break;
784
796
  case 'use-function-declaration':
785
- suggestions.push({
786
- violation: violation.rule,
787
- 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.',
788
799
  example: `// ❌ WRONG - Top-level arrow function component:
789
800
  const MyComponent = ({ utilities, styles, components }) => {
790
801
  const [state, setState] = useState('');
@@ -817,12 +828,11 @@ function ChildComponent() {
817
828
  // 3. Hoisting allows flexible code organization
818
829
  // 4. Consistent with React documentation patterns
819
830
  // 5. Easier to distinguish from regular variables`
820
- });
831
+ };
821
832
  break;
822
833
  case 'no-return-component':
823
- suggestions.push({
824
- violation: violation.rule,
825
- 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.',
826
836
  example: `// ❌ WRONG - Returning the component:
827
837
  function MyComponent({ utilities, styles, components }) {
828
838
  const [state, setState] = useState('');
@@ -849,12 +859,11 @@ function MyComponent({ utilities, styles, components }) {
849
859
 
850
860
  // The runtime will find and execute your component
851
861
  // by its function name. No need to return or reference it!`
852
- });
862
+ };
853
863
  break;
854
864
  case 'no-iife-wrapper':
855
- suggestions.push({
856
- violation: violation.rule,
857
- 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.',
858
867
  example: `// ❌ WRONG - IIFE wrapper patterns:
859
868
  (function() {
860
869
  function MyComponent({ utilities, styles, components }) {
@@ -888,12 +897,11 @@ function MyComponent({ utilities, styles, components }) {
888
897
  // 3. IIFEs prevent proper component discovery
889
898
  // 4. Makes debugging harder
890
899
  // 5. Unnecessary complexity`
891
- });
900
+ };
892
901
  break;
893
902
  case 'full-state-ownership':
894
- suggestions.push({
895
- violation: violation.rule,
896
- 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.',
897
905
  example: `// ❌ WRONG - Controlled state props:
898
906
  function PaginationControls({ currentPage, filters, sortBy, onPageChange }) {
899
907
  // These props suggest parent controls the state - WRONG!
@@ -953,12 +961,11 @@ function DataTable({
953
961
  // - Direct state names (currentPage, selectedId, activeTab)
954
962
  // - State without 'initial'/'default' prefix (sortBy, filters, searchTerm)
955
963
  // - Controlled patterns (value + onChange, checked + onChange)`
956
- });
964
+ };
957
965
  break;
958
966
  case 'no-use-reducer':
959
- suggestions.push({
960
- violation: violation.rule,
961
- suggestion: 'Use useState for state management, not useReducer',
967
+ violation.suggestion = {
968
+ text: 'Use useState for state management, not useReducer',
962
969
  example: `// Instead of:
963
970
  const [state, dispatch] = useReducer(reducer, initialState);
964
971
 
@@ -981,12 +988,11 @@ function Component({ savedUserSettings, onSaveUserSettings }) {
981
988
  }
982
989
  };
983
990
  }`
984
- });
991
+ };
985
992
  break;
986
993
  case 'no-data-prop':
987
- suggestions.push({
988
- violation: violation.rule,
989
- suggestion: 'Replace generic data prop with specific named props',
994
+ violation.suggestion = {
995
+ text: 'Replace generic data prop with specific named props',
990
996
  example: `// Instead of:
991
997
  function Component({ data, savedUserSettings, onSaveUserSettings }) {
992
998
  return <div>{data.items.map(...)}</div>;
@@ -1004,12 +1010,11 @@ function Component({ items, customers, savedUserSettings, onSaveUserSettings })
1004
1010
 
1005
1011
  // Load data using utilities:
1006
1012
  const result = await utilities.rv.RunView({ entityName: 'Items' });`
1007
- });
1013
+ };
1008
1014
  break;
1009
1015
  case 'saved-user-settings-pattern':
1010
- suggestions.push({
1011
- violation: violation.rule,
1012
- suggestion: 'Only save important user preferences, not ephemeral UI state',
1016
+ violation.suggestion = {
1017
+ text: 'Only save important user preferences, not ephemeral UI state',
1013
1018
  example: `// ✅ SAVE these (important preferences):
1014
1019
  - Selected items/tabs: selectedCustomerId, activeTab
1015
1020
  - Sort preferences: sortBy, sortDirection
@@ -1034,12 +1039,11 @@ const handleSelect = (id) => {
1034
1039
  selectedId: id
1035
1040
  });
1036
1041
  };`
1037
- });
1042
+ };
1038
1043
  break;
1039
1044
  case 'pass-standard-props':
1040
- suggestions.push({
1041
- violation: violation.rule,
1042
- suggestion: 'Always pass standard props to all components',
1045
+ violation.suggestion = {
1046
+ text: 'Always pass standard props to all components',
1043
1047
  example: `// Always include these props when calling components:
1044
1048
  <ChildComponent
1045
1049
  items={items} // Data props
@@ -1054,19 +1058,17 @@ const handleSelect = (id) => {
1054
1058
  components={components}
1055
1059
  callbacks={callbacks}
1056
1060
  />`
1057
- });
1061
+ };
1058
1062
  break;
1059
1063
  case 'no-child-implementation':
1060
- suggestions.push({
1061
- violation: violation.rule,
1062
- 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',
1063
1066
  example: 'Move child component functions to separate generation requests'
1064
- });
1067
+ };
1065
1068
  break;
1066
1069
  case 'undefined-component-usage':
1067
- suggestions.push({
1068
- violation: violation.rule,
1069
- 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',
1070
1072
  example: `// Component spec should include all referenced components:
1071
1073
  {
1072
1074
  "name": "MyComponent",
@@ -1091,12 +1093,11 @@ const handleSelect = (id) => {
1091
1093
  // Then in your component:
1092
1094
  const { ModelTreeView, PromptTable, FilterPanel } = components;
1093
1095
  // All these will be available`
1094
- });
1096
+ };
1095
1097
  break;
1096
1098
  case 'component-usage-without-destructuring':
1097
- suggestions.push({
1098
- violation: violation.rule,
1099
- 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',
1100
1101
  example: `// ❌ WRONG - Using component without destructuring:
1101
1102
  function MyComponent({ components }) {
1102
1103
  return <AccountList />; // Error: AccountList not destructured
@@ -1117,12 +1118,11 @@ function MyComponent({ components }) {
1117
1118
  function MyComponent({ components: { AccountList } }) {
1118
1119
  return <AccountList />;
1119
1120
  }`
1120
- });
1121
+ };
1121
1122
  break;
1122
1123
  case 'unsafe-array-access':
1123
- suggestions.push({
1124
- violation: violation.rule,
1125
- suggestion: 'Always check array bounds before accessing elements',
1124
+ violation.suggestion = {
1125
+ text: 'Always check array bounds before accessing elements',
1126
1126
  example: `// ❌ UNSAFE:
1127
1127
  const firstItem = items[0].name;
1128
1128
  const total = data[0].reduce((sum, item) => sum + item.value, 0);
@@ -1136,12 +1136,11 @@ const total = data.length > 0
1136
1136
  // ✅ BETTER - Use optional chaining:
1137
1137
  const firstItem = items[0]?.name || 'No items';
1138
1138
  const total = data[0]?.reduce((sum, item) => sum + item.value, 0) || 0;`
1139
- });
1139
+ };
1140
1140
  break;
1141
1141
  case 'array-reduce-safety':
1142
- suggestions.push({
1143
- violation: violation.rule,
1144
- 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',
1145
1144
  example: `// ❌ UNSAFE:
1146
1145
  const sum = numbers.reduce((a, b) => a + b); // Fails on empty array
1147
1146
  const total = data[0].reduce((sum, item) => sum + item.value); // Multiple issues
@@ -1156,12 +1155,11 @@ const total = data.length > 0 && data[0]
1156
1155
  const sum = numbers.length > 0
1157
1156
  ? numbers.reduce((a, b) => a + b)
1158
1157
  : 0;`
1159
- });
1158
+ };
1160
1159
  break;
1161
1160
  case 'entity-name-mismatch':
1162
- suggestions.push({
1163
- violation: violation.rule,
1164
- 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',
1165
1163
  example: `// The component spec defines the entities to use:
1166
1164
  // dataRequirements: {
1167
1165
  // entities: [
@@ -1189,12 +1187,11 @@ await utilities.rv.RunViews([
1189
1187
 
1190
1188
  // The linter validates that all entity names in RunView/RunViews calls
1191
1189
  // match those declared in the component spec's dataRequirements`
1192
- });
1190
+ };
1193
1191
  break;
1194
1192
  case 'missing-query-parameter':
1195
- suggestions.push({
1196
- violation: violation.rule,
1197
- 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',
1198
1195
  example: `// The component spec defines required parameters:
1199
1196
  // dataRequirements: {
1200
1197
  // queries: [
@@ -1225,12 +1222,11 @@ await utilities.rq.RunQuery({
1225
1222
  StartDate: startDate // All parameters included
1226
1223
  }
1227
1224
  });`
1228
- });
1225
+ };
1229
1226
  break;
1230
1227
  case 'unknown-query-parameter':
1231
- suggestions.push({
1232
- violation: violation.rule,
1233
- 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',
1234
1230
  example: `// ❌ WRONG - Using undefined parameter:
1235
1231
  await utilities.rq.RunQuery({
1236
1232
  QueryName: "User Activity Summary",
@@ -1249,12 +1245,11 @@ await utilities.rq.RunQuery({
1249
1245
  StartDate: startDate // Only parameters from dataRequirements
1250
1246
  }
1251
1247
  });`
1252
- });
1248
+ };
1253
1249
  break;
1254
1250
  case 'missing-parameters-object':
1255
- suggestions.push({
1256
- violation: violation.rule,
1257
- 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',
1258
1253
  example: `// ❌ WRONG - Query requires parameters but none provided:
1259
1254
  await utilities.rq.RunQuery({
1260
1255
  QueryName: "User Activity Summary"
@@ -1269,12 +1264,11 @@ await utilities.rq.RunQuery({
1269
1264
  StartDate: startDate
1270
1265
  }
1271
1266
  });`
1272
- });
1267
+ };
1273
1268
  break;
1274
1269
  case 'query-name-mismatch':
1275
- suggestions.push({
1276
- violation: violation.rule,
1277
- 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',
1278
1272
  example: `// The component spec defines the queries to use:
1279
1273
  // dataRequirements: {
1280
1274
  // queries: [
@@ -1296,12 +1290,11 @@ await utilities.rv.RunQuery({
1296
1290
 
1297
1291
  // The linter validates that all query names in RunQuery calls
1298
1292
  // match those declared in the component spec's dataRequirements.queries`
1299
- });
1293
+ };
1300
1294
  break;
1301
1295
  case 'runview-sql-function':
1302
- suggestions.push({
1303
- violation: violation.rule,
1304
- 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.',
1305
1298
  example: `// ❌ WRONG - SQL functions in RunView:
1306
1299
  await utilities.rv.RunView({
1307
1300
  EntityName: 'Accounts',
@@ -1323,12 +1316,11 @@ if (result?.Success) {
1323
1316
  const total = result.Results.length;
1324
1317
  const totalRevenue = result.Results.reduce((sum, acc) => sum + (acc.Revenue || 0), 0);
1325
1318
  }`
1326
- });
1319
+ };
1327
1320
  break;
1328
1321
  case 'field-not-in-requirements':
1329
- suggestions.push({
1330
- violation: violation.rule,
1331
- 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',
1332
1324
  example: `// Check your dataRequirements to see allowed fields:
1333
1325
  // dataRequirements: {
1334
1326
  // entities: [{
@@ -1350,12 +1342,11 @@ await utilities.rv.RunView({
1350
1342
  EntityName: 'Accounts',
1351
1343
  Fields: ['ID', 'AccountName', 'Industry'] // All from displayFields
1352
1344
  });`
1353
- });
1345
+ };
1354
1346
  break;
1355
1347
  case 'orderby-field-not-sortable':
1356
- suggestions.push({
1357
- violation: violation.rule,
1358
- 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',
1359
1350
  example: `// ❌ WRONG - Sorting by non-sortable field:
1360
1351
  await utilities.rv.RunView({
1361
1352
  EntityName: 'Accounts',
@@ -1367,12 +1358,11 @@ await utilities.rv.RunView({
1367
1358
  EntityName: 'Accounts',
1368
1359
  OrderBy: 'AccountName ASC' // AccountName is in sortFields
1369
1360
  });`
1370
- });
1361
+ };
1371
1362
  break;
1372
1363
  case 'parent-event-callback-usage':
1373
- suggestions.push({
1374
- violation: violation.rule,
1375
- suggestion: 'Components must invoke parent event callbacks when state changes',
1364
+ violation.suggestion = {
1365
+ text: 'Components must invoke parent event callbacks when state changes',
1376
1366
  example: `// ❌ WRONG - Only updating internal state:
1377
1367
  function ChildComponent({ onSelectAccount, savedUserSettings, onSaveUserSettings }) {
1378
1368
  const [selectedAccountId, setSelectedAccountId] = useState(savedUserSettings?.selectedAccountId);
@@ -1401,12 +1391,11 @@ function ChildComponent({ onSelectAccount, savedUserSettings, onSaveUserSettings
1401
1391
  onSaveUserSettings?.({ ...savedUserSettings, selectedAccountId: accountId });
1402
1392
  };
1403
1393
  }`
1404
- });
1394
+ };
1405
1395
  break;
1406
1396
  case 'property-name-consistency':
1407
- suggestions.push({
1408
- violation: violation.rule,
1409
- suggestion: 'Maintain consistent property names when transforming data',
1397
+ violation.suggestion = {
1398
+ text: 'Maintain consistent property names when transforming data',
1410
1399
  example: `// ❌ WRONG - Transform to camelCase but access as PascalCase:
1411
1400
  setAccountData(results.map(item => ({
1412
1401
  accountName: item.AccountName, // camelCase
@@ -1433,12 +1422,11 @@ setAccountData(results.map(item => ({
1433
1422
  // Later in render...
1434
1423
  <td>{account.accountName}</td> // Use camelCase consistently
1435
1424
  <td>{formatCurrency(account.annualRevenue)}</td> // Works!`
1436
- });
1425
+ };
1437
1426
  break;
1438
1427
  case 'noisy-settings-updates':
1439
- suggestions.push({
1440
- violation: violation.rule,
1441
- suggestion: 'Save settings sparingly - only on meaningful user actions',
1428
+ violation.suggestion = {
1429
+ text: 'Save settings sparingly - only on meaningful user actions',
1442
1430
  example: `// ❌ WRONG - Saving on every keystroke:
1443
1431
  const handleSearchChange = (e) => {
1444
1432
  setSearchTerm(e.target.value);
@@ -1459,12 +1447,11 @@ const saveSearchTerm = useMemo(() =>
1459
1447
  }, 500),
1460
1448
  [savedUserSettings]
1461
1449
  );`
1462
- });
1450
+ };
1463
1451
  break;
1464
1452
  case 'prop-state-sync':
1465
- suggestions.push({
1466
- violation: violation.rule,
1467
- suggestion: 'Initialize state once, don\'t sync from props',
1453
+ violation.suggestion = {
1454
+ text: 'Initialize state once, don\'t sync from props',
1468
1455
  example: `// ❌ WRONG - Syncing prop to state:
1469
1456
  const [value, setValue] = useState(propValue);
1470
1457
  useEffect(() => {
@@ -1478,12 +1465,11 @@ const [value, setValue] = useState(
1478
1465
 
1479
1466
  // ✅ CORRECT - If you need prop changes, use derived state:
1480
1467
  const displayValue = propOverride || value;`
1481
- });
1468
+ };
1482
1469
  break;
1483
1470
  case 'performance-memoization':
1484
- suggestions.push({
1485
- violation: violation.rule,
1486
- suggestion: 'Use useMemo for expensive operations and static data',
1471
+ violation.suggestion = {
1472
+ text: 'Use useMemo for expensive operations and static data',
1487
1473
  example: `// ❌ WRONG - Expensive operation on every render:
1488
1474
  const filteredItems = items.filter(item =>
1489
1475
  item.name.toLowerCase().includes(searchTerm.toLowerCase())
@@ -1508,12 +1494,11 @@ const columns = useMemo(() => [
1508
1494
  { field: 'name', header: 'Name' },
1509
1495
  { field: 'value', header: 'Value' }
1510
1496
  ], []); // Empty deps = never changes`
1511
- });
1497
+ };
1512
1498
  break;
1513
1499
  case 'child-state-management':
1514
- suggestions.push({
1515
- violation: violation.rule,
1516
- suggestion: 'Never manage state for child components',
1500
+ violation.suggestion = {
1501
+ text: 'Never manage state for child components',
1517
1502
  example: `// ❌ WRONG - Managing child state:
1518
1503
  const [childTableSort, setChildTableSort] = useState('name');
1519
1504
  const [modalOpen, setModalOpen] = useState(false);
@@ -1530,12 +1515,11 @@ const [modalOpen, setModalOpen] = useState(false);
1530
1515
  onSaveUserSettings={handleChildSettings}
1531
1516
  // Child manages its own sort state!
1532
1517
  />`
1533
- });
1518
+ };
1534
1519
  break;
1535
1520
  case 'server-reload-on-client-operation':
1536
- suggestions.push({
1537
- violation: violation.rule,
1538
- suggestion: 'Use client-side operations for sorting and filtering',
1521
+ violation.suggestion = {
1522
+ text: 'Use client-side operations for sorting and filtering',
1539
1523
  example: `// ❌ WRONG - Reload from server:
1540
1524
  const handleSort = (field) => {
1541
1525
  setSortBy(field);
@@ -1559,12 +1543,11 @@ const sortedData = useMemo(() => {
1559
1543
  });
1560
1544
  return sorted;
1561
1545
  }, [data, sortBy, sortDirection]);`
1562
- });
1546
+ };
1563
1547
  break;
1564
1548
  case 'runview-runquery-valid-properties':
1565
- suggestions.push({
1566
- violation: violation.rule,
1567
- suggestion: 'Use only valid properties for RunView/RunViews and RunQuery',
1549
+ violation.suggestion = {
1550
+ text: 'Use only valid properties for RunView/RunViews and RunQuery',
1568
1551
  example: `// ❌ WRONG - Invalid properties on RunView:
1569
1552
  await utilities.rv.RunView({
1570
1553
  EntityName: 'MJ: AI Prompt Runs',
@@ -1593,23 +1576,34 @@ await utilities.rq.RunQuery({
1593
1576
  // Valid RunQuery properties:
1594
1577
  // - QueryName (required)
1595
1578
  // - CategoryName, CategoryID, Parameters (optional)`
1596
- });
1579
+ };
1597
1580
  break;
1598
1581
  case 'component-props-validation':
1599
- suggestions.push({
1600
- violation: violation.rule,
1601
- suggestion: 'Components can only accept standard props or props defined in spec. Load data internally.',
1602
- example: `// WRONG - Root component with additional props:
1603
- function RootComponent({ utilities, styles, components, customers, orders, selectedId }) {
1604
- // 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
1605
1587
  }
1606
1588
 
1607
- // ✅ CORRECT - Root component with only standard props:
1608
- function RootComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
1609
- // 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
1610
1592
  const [customers, setCustomers] = useState([]);
1611
1593
  const [orders, setOrders] = useState([]);
1612
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
1613
1607
 
1614
1608
  useEffect(() => {
1615
1609
  const loadData = async () => {
@@ -1630,12 +1624,11 @@ function RootComponent({ utilities, styles, components, callbacks, savedUserSett
1630
1624
 
1631
1625
  return <div>{/* Use state, not props */}</div>;
1632
1626
  }`
1633
- });
1627
+ };
1634
1628
  break;
1635
1629
  case 'runview-runquery-result-direct-usage':
1636
- suggestions.push({
1637
- violation: violation.rule,
1638
- 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.',
1639
1632
  example: `// ❌ WRONG - Using result directly as array:
1640
1633
  const result = await utilities.rv.RunView({
1641
1634
  EntityName: 'Users',
@@ -1681,45 +1674,77 @@ setData(queryResult.Results || []); // NOT queryResult directly!
1681
1674
  // TotalRowCount?: number,
1682
1675
  // ExecutionTime?: number
1683
1676
  // }`
1684
- });
1677
+ };
1678
+ break;
1679
+ case 'styles-invalid-path':
1680
+ violation.suggestion = {
1681
+ text: 'Fix invalid styles property paths. Use the correct ComponentStyles interface structure.',
1682
+ example: `// ❌ WRONG - Invalid property paths:
1683
+ styles.fontSize.small // fontSize is not at root level
1684
+ styles.colors.background // colors.background exists
1685
+ styles.spacing.small // should be styles.spacing.sm
1686
+
1687
+ // ✅ CORRECT - Valid property paths:
1688
+ styles.typography.fontSize.sm // fontSize is under typography
1689
+ styles.colors.background // correct path
1690
+ styles.spacing.sm // correct size name
1691
+
1692
+ // With safe access and fallbacks:
1693
+ styles?.typography?.fontSize?.sm || '14px'
1694
+ styles?.colors?.background || '#FFFFFF'
1695
+ styles?.spacing?.sm || '8px'`
1696
+ };
1697
+ break;
1698
+ case 'styles-unsafe-access':
1699
+ violation.suggestion = {
1700
+ text: 'Use optional chaining for nested styles access to prevent runtime errors.',
1701
+ example: `// ❌ UNSAFE - Direct nested access:
1702
+ const fontSize = styles.typography.fontSize.md;
1703
+ const borderRadius = styles.borders.radius.sm;
1704
+
1705
+ // ✅ SAFE - With optional chaining and fallbacks:
1706
+ const fontSize = styles?.typography?.fontSize?.md || '14px';
1707
+ const borderRadius = styles?.borders?.radius?.sm || '6px';
1708
+
1709
+ // Even better - destructure with defaults:
1710
+ const {
1711
+ typography: {
1712
+ fontSize: { md: fontSize = '14px' } = {}
1713
+ } = {}
1714
+ } = styles || {};`
1715
+ };
1685
1716
  break;
1686
1717
  }
1687
1718
  }
1688
- return suggestions;
1719
+ return violations;
1689
1720
  }
1690
1721
  static generateSyntaxErrorSuggestions(violations) {
1691
- const suggestions = [];
1692
1722
  for (const violation of violations) {
1693
1723
  if (violation.message.includes('Unterminated string')) {
1694
- suggestions.push({
1695
- violation: violation.rule,
1696
- 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',
1697
1726
  example: 'Template literals with interpolation must use backticks: `text ${variable} text`'
1698
- });
1727
+ };
1699
1728
  }
1700
1729
  else if (violation.message.includes('Unexpected token') || violation.message.includes('export')) {
1701
- suggestions.push({
1702
- violation: violation.rule,
1703
- suggestion: 'Ensure all code is within the component function body',
1730
+ violation.suggestion = {
1731
+ text: 'Ensure all code is within the component function body',
1704
1732
  example: 'Remove any export statements or code outside the function definition'
1705
- });
1733
+ };
1706
1734
  }
1707
1735
  else if (violation.message.includes('import') && violation.message.includes('top level')) {
1708
- suggestions.push({
1709
- violation: violation.rule,
1710
- 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',
1711
1738
  example: 'Access libraries through props: const { React, MaterialUI } = props.components'
1712
- });
1739
+ };
1713
1740
  }
1714
1741
  else {
1715
- suggestions.push({
1716
- violation: violation.rule,
1717
- 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',
1718
1744
  example: 'Review the code at the specified line and column for syntax issues'
1719
- });
1745
+ };
1720
1746
  }
1721
1747
  }
1722
- return suggestions;
1723
1748
  }
1724
1749
  /**
1725
1750
  * Apply library-specific lint rules based on ComponentLibrary LintRules field
@@ -2464,7 +2489,7 @@ ComponentLinter.universalComponentRules = [
2464
2489
  severity: 'critical',
2465
2490
  line: path.node.loc?.start.line || 0,
2466
2491
  column: path.node.loc?.start.column || 0,
2467
- 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.`,
2468
2493
  code: `const ${varName} = ...`
2469
2494
  });
2470
2495
  }
@@ -2482,15 +2507,15 @@ ComponentLinter.universalComponentRules = [
2482
2507
  severity: 'critical',
2483
2508
  line: path.node.loc?.start.line || 0,
2484
2509
  column: path.node.loc?.start.column || 0,
2485
- 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.`,
2486
2511
  code: `function ${funcName}(...)`
2487
2512
  });
2488
2513
  }
2489
2514
  }
2490
2515
  }
2491
2516
  });
2492
- // Components are now auto-destructured in the wrapper, so we don't need to check for manual destructuring
2493
- // 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
2494
2519
  let hasComponentsUsage = false;
2495
2520
  const usedDependencies = new Set();
2496
2521
  mainComponentPath.traverse({
@@ -2531,7 +2556,7 @@ ComponentLinter.universalComponentRules = [
2531
2556
  }
2532
2557
  }
2533
2558
  });
2534
- // 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
2535
2560
  if (dependencyNames.size > 0 && usedDependencies.size === 0) {
2536
2561
  const depList = Array.from(dependencyNames).join(', ');
2537
2562
  violations.push({
@@ -2539,7 +2564,7 @@ ComponentLinter.universalComponentRules = [
2539
2564
  severity: 'low',
2540
2565
  line: mainComponentPath.node.loc?.start.line || 0,
2541
2566
  column: mainComponentPath.node.loc?.start.column || 0,
2542
- 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.`,
2543
2568
  code: `// Available: ${depList}`
2544
2569
  });
2545
2570
  }
@@ -4044,6 +4069,117 @@ Valid properties: EntityName, ExtraFilter, Fields, OrderBy, MaxRows, StartRow, R
4044
4069
  code: `${propName}: ...`
4045
4070
  });
4046
4071
  }
4072
+ else {
4073
+ // Property name is valid, now check its type
4074
+ const value = prop.value;
4075
+ // Helper to check if a node is null or undefined
4076
+ const isNullOrUndefined = (node) => {
4077
+ return t.isNullLiteral(node) ||
4078
+ (t.isIdentifier(node) && node.name === 'undefined');
4079
+ };
4080
+ // Helper to check if a node could evaluate to a string
4081
+ const isStringLike = (node, depth = 0) => {
4082
+ // Prevent infinite recursion
4083
+ if (depth > 3)
4084
+ return false;
4085
+ // Special handling for ternary operators - check both branches
4086
+ if (t.isConditionalExpression(node)) {
4087
+ const consequentOk = isStringLike(node.consequent, depth + 1) || isNullOrUndefined(node.consequent);
4088
+ const alternateOk = isStringLike(node.alternate, depth + 1) || isNullOrUndefined(node.alternate);
4089
+ return consequentOk && alternateOk;
4090
+ }
4091
+ // Explicitly reject object and array expressions
4092
+ if (t.isObjectExpression(node) || t.isArrayExpression(node)) {
4093
+ return false;
4094
+ }
4095
+ return t.isStringLiteral(node) ||
4096
+ t.isTemplateLiteral(node) ||
4097
+ t.isBinaryExpression(node) || // String concatenation
4098
+ t.isIdentifier(node) || // Variable
4099
+ t.isCallExpression(node) || // Function call
4100
+ t.isMemberExpression(node); // Property access
4101
+ };
4102
+ // Helper to check if a node could evaluate to a number
4103
+ const isNumberLike = (node) => {
4104
+ return t.isNumericLiteral(node) ||
4105
+ t.isBinaryExpression(node) || // Math operations
4106
+ t.isUnaryExpression(node) || // Negative numbers, etc
4107
+ t.isConditionalExpression(node) || // Ternary
4108
+ t.isIdentifier(node) || // Variable
4109
+ t.isCallExpression(node) || // Function call
4110
+ t.isMemberExpression(node); // Property access
4111
+ };
4112
+ // Helper to check if a node is array-like
4113
+ const isArrayLike = (node) => {
4114
+ return t.isArrayExpression(node) ||
4115
+ t.isIdentifier(node) || // Variable
4116
+ t.isCallExpression(node) || // Function returning array
4117
+ t.isMemberExpression(node) || // Property access
4118
+ t.isConditionalExpression(node); // Ternary
4119
+ };
4120
+ // Helper to check if a node is object-like (but not array)
4121
+ const isObjectLike = (node) => {
4122
+ if (t.isArrayExpression(node))
4123
+ return false;
4124
+ return t.isObjectExpression(node) ||
4125
+ t.isIdentifier(node) || // Variable
4126
+ t.isCallExpression(node) || // Function returning object
4127
+ t.isMemberExpression(node) || // Property access
4128
+ t.isConditionalExpression(node) || // Ternary
4129
+ t.isSpreadElement(node); // Spread syntax (though this is the problem case)
4130
+ };
4131
+ // Validate types based on property name
4132
+ if (propName === 'ExtraFilter' || propName === 'OrderBy' || propName === 'EntityName') {
4133
+ // These must be strings (ExtraFilter and OrderBy can also be null/undefined)
4134
+ const allowNullUndefined = propName === 'ExtraFilter' || propName === 'OrderBy';
4135
+ if (!isStringLike(value) && !(allowNullUndefined && isNullOrUndefined(value))) {
4136
+ let exampleValue = '';
4137
+ if (propName === 'ExtraFilter') {
4138
+ exampleValue = `"Status = 'Active' AND Type = 'Customer'"`;
4139
+ }
4140
+ else if (propName === 'OrderBy') {
4141
+ exampleValue = `"CreatedAt DESC"`;
4142
+ }
4143
+ else if (propName === 'EntityName') {
4144
+ exampleValue = `"Products"`;
4145
+ }
4146
+ violations.push({
4147
+ rule: 'runview-runquery-valid-properties',
4148
+ severity: 'critical',
4149
+ line: prop.loc?.start.line || 0,
4150
+ column: prop.loc?.start.column || 0,
4151
+ message: `${methodName} property '${propName}' must be a string, not ${t.isObjectExpression(value) ? 'an object' : t.isArrayExpression(value) ? 'an array' : 'a non-string value'}. Example: ${propName}: ${exampleValue}`,
4152
+ code: `${propName}: ${prop.value.type === 'ObjectExpression' ? '{...}' : prop.value.type === 'ArrayExpression' ? '[...]' : '...'}`
4153
+ });
4154
+ }
4155
+ }
4156
+ else if (propName === 'Fields') {
4157
+ // Fields must be an array of strings (or a string that we'll interpret as comma-separated)
4158
+ if (!isArrayLike(value) && !isStringLike(value)) {
4159
+ violations.push({
4160
+ rule: 'runview-runquery-valid-properties',
4161
+ severity: 'critical',
4162
+ line: prop.loc?.start.line || 0,
4163
+ column: prop.loc?.start.column || 0,
4164
+ message: `${methodName} property 'Fields' must be an array of field names or a comma-separated string. Example: Fields: ['ID', 'Name', 'Status'] or Fields: 'ID, Name, Status'`,
4165
+ code: `Fields: ${prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
4166
+ });
4167
+ }
4168
+ }
4169
+ else if (propName === 'MaxRows' || propName === 'StartRow') {
4170
+ // These must be numbers
4171
+ if (!isNumberLike(value)) {
4172
+ violations.push({
4173
+ rule: 'runview-runquery-valid-properties',
4174
+ severity: 'critical',
4175
+ line: prop.loc?.start.line || 0,
4176
+ column: prop.loc?.start.column || 0,
4177
+ message: `${methodName} property '${propName}' must be a number. Example: ${propName}: ${propName === 'MaxRows' ? '100' : '0'}`,
4178
+ code: `${propName}: ${prop.value.type === 'StringLiteral' ? '"..."' : prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
4179
+ });
4180
+ }
4181
+ }
4182
+ }
4047
4183
  }
4048
4184
  }
4049
4185
  // Check that EntityName is present (required property)
@@ -4148,6 +4284,111 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4148
4284
  code: `${propName}: ...`
4149
4285
  });
4150
4286
  }
4287
+ else {
4288
+ // Property name is valid, now check its type
4289
+ const value = prop.value;
4290
+ // Helper to check if a node is null or undefined
4291
+ const isNullOrUndefined = (node) => {
4292
+ return t.isNullLiteral(node) ||
4293
+ (t.isIdentifier(node) && node.name === 'undefined');
4294
+ };
4295
+ // Helper to check if a node could evaluate to a string
4296
+ const isStringLike = (node, depth = 0) => {
4297
+ // Prevent infinite recursion
4298
+ if (depth > 3)
4299
+ return false;
4300
+ // Special handling for ternary operators - check both branches
4301
+ if (t.isConditionalExpression(node)) {
4302
+ const consequentOk = isStringLike(node.consequent, depth + 1) || isNullOrUndefined(node.consequent);
4303
+ const alternateOk = isStringLike(node.alternate, depth + 1) || isNullOrUndefined(node.alternate);
4304
+ return consequentOk && alternateOk;
4305
+ }
4306
+ // Explicitly reject object and array expressions
4307
+ if (t.isObjectExpression(node) || t.isArrayExpression(node)) {
4308
+ return false;
4309
+ }
4310
+ return t.isStringLiteral(node) ||
4311
+ t.isTemplateLiteral(node) ||
4312
+ t.isBinaryExpression(node) || // String concatenation
4313
+ t.isIdentifier(node) || // Variable
4314
+ t.isCallExpression(node) || // Function call
4315
+ t.isMemberExpression(node); // Property access
4316
+ };
4317
+ // Helper to check if a node could evaluate to a number
4318
+ const isNumberLike = (node) => {
4319
+ return t.isNumericLiteral(node) ||
4320
+ t.isBinaryExpression(node) || // Math operations
4321
+ t.isUnaryExpression(node) || // Negative numbers, etc
4322
+ t.isConditionalExpression(node) || // Ternary
4323
+ t.isIdentifier(node) || // Variable
4324
+ t.isCallExpression(node) || // Function call
4325
+ t.isMemberExpression(node); // Property access
4326
+ };
4327
+ // Helper to check if a node is object-like (but not array)
4328
+ const isObjectLike = (node) => {
4329
+ if (t.isArrayExpression(node))
4330
+ return false;
4331
+ return t.isObjectExpression(node) ||
4332
+ t.isIdentifier(node) || // Variable
4333
+ t.isCallExpression(node) || // Function returning object
4334
+ t.isMemberExpression(node) || // Property access
4335
+ t.isConditionalExpression(node) || // Ternary
4336
+ t.isSpreadElement(node); // Spread syntax
4337
+ };
4338
+ // Validate types based on property name
4339
+ if (propName === 'QueryID' || propName === 'QueryName' || propName === 'CategoryID' || propName === 'CategoryPath') {
4340
+ // These must be strings
4341
+ if (!isStringLike(value)) {
4342
+ let exampleValue = '';
4343
+ if (propName === 'QueryID') {
4344
+ exampleValue = `"550e8400-e29b-41d4-a716-446655440000"`;
4345
+ }
4346
+ else if (propName === 'QueryName') {
4347
+ exampleValue = `"Sales by Region"`;
4348
+ }
4349
+ else if (propName === 'CategoryID') {
4350
+ exampleValue = `"123e4567-e89b-12d3-a456-426614174000"`;
4351
+ }
4352
+ else if (propName === 'CategoryPath') {
4353
+ exampleValue = `"/Reports/Sales/"`;
4354
+ }
4355
+ violations.push({
4356
+ rule: 'runview-runquery-valid-properties',
4357
+ severity: 'critical',
4358
+ line: prop.loc?.start.line || 0,
4359
+ column: prop.loc?.start.column || 0,
4360
+ message: `RunQuery property '${propName}' must be a string. Example: ${propName}: ${exampleValue}`,
4361
+ code: `${propName}: ${prop.value.type === 'ObjectExpression' ? '{...}' : prop.value.type === 'ArrayExpression' ? '[...]' : '...'}`
4362
+ });
4363
+ }
4364
+ }
4365
+ else if (propName === 'Parameters') {
4366
+ // Parameters must be an object (Record<string, any>)
4367
+ if (!isObjectLike(value)) {
4368
+ violations.push({
4369
+ rule: 'runview-runquery-valid-properties',
4370
+ severity: 'critical',
4371
+ line: prop.loc?.start.line || 0,
4372
+ column: prop.loc?.start.column || 0,
4373
+ message: `RunQuery property 'Parameters' must be an object containing key-value pairs. Example: Parameters: { startDate: '2024-01-01', status: 'Active' }`,
4374
+ code: `Parameters: ${t.isArrayExpression(value) ? '[...]' : t.isStringLiteral(value) ? '"..."' : '...'}`
4375
+ });
4376
+ }
4377
+ }
4378
+ else if (propName === 'MaxRows' || propName === 'StartRow') {
4379
+ // These must be numbers
4380
+ if (!isNumberLike(value)) {
4381
+ violations.push({
4382
+ rule: 'runview-runquery-valid-properties',
4383
+ severity: 'critical',
4384
+ line: prop.loc?.start.line || 0,
4385
+ column: prop.loc?.start.column || 0,
4386
+ message: `RunQuery property '${propName}' must be a number. Example: ${propName}: ${propName === 'MaxRows' ? '100' : '0'}`,
4387
+ code: `${propName}: ${prop.value.type === 'StringLiteral' ? '"..."' : prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
4388
+ });
4389
+ }
4390
+ }
4391
+ }
4151
4392
  }
4152
4393
  }
4153
4394
  // Check that at least one required property is present
@@ -4684,13 +4925,18 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4684
4925
  test: (ast, componentName, componentSpec) => {
4685
4926
  const violations = [];
4686
4927
  const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
4687
- // Build set of allowed props: standard props + componentSpec properties
4688
- const allowedProps = new Set(standardProps);
4928
+ // React special props that are automatically provided by React
4929
+ const reactSpecialProps = new Set(['children']);
4930
+ // Build set of allowed props: standard props + React special props + componentSpec properties
4931
+ const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
4689
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 = [];
4690
4935
  if (componentSpec?.properties) {
4691
4936
  for (const prop of componentSpec.properties) {
4692
4937
  if (prop.name) {
4693
4938
  allowedProps.add(prop.name);
4939
+ specDefinedProps.push(prop.name);
4694
4940
  }
4695
4941
  }
4696
4942
  }
@@ -4712,15 +4958,28 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4712
4958
  }
4713
4959
  // Only report if there are non-allowed props
4714
4960
  if (invalidProps.length > 0) {
4715
- const customPropsMessage = componentSpec?.properties?.length
4716
- ? ` and custom props defined in spec: ${componentSpec.properties.map(p => p.name).join(', ')}`
4717
- : '';
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
+ }
4718
4977
  violations.push({
4719
4978
  rule: 'component-props-validation',
4720
4979
  severity: 'critical',
4721
4980
  line: path.node.loc?.start.line || 0,
4722
4981
  column: path.node.loc?.start.column || 0,
4723
- message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
4982
+ message
4724
4983
  });
4725
4984
  }
4726
4985
  }
@@ -4745,15 +5004,28 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4745
5004
  }
4746
5005
  }
4747
5006
  if (invalidProps.length > 0) {
4748
- const customPropsMessage = componentSpec?.properties?.length
4749
- ? ` and custom props defined in spec: ${componentSpec.properties.map(p => p.name).join(', ')}`
4750
- : '';
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
+ }
4751
5023
  violations.push({
4752
5024
  rule: 'component-props-validation',
4753
5025
  severity: 'critical',
4754
5026
  line: path.node.loc?.start.line || 0,
4755
5027
  column: path.node.loc?.start.column || 0,
4756
- message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
5028
+ message
4757
5029
  });
4758
5030
  }
4759
5031
  }
@@ -4765,78 +5037,219 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4765
5037
  }
4766
5038
  },
4767
5039
  {
4768
- name: 'invalid-components-destructuring',
5040
+ name: 'validate-dependency-props',
4769
5041
  appliesTo: 'all',
4770
5042
  test: (ast, componentName, componentSpec) => {
4771
5043
  const violations = [];
4772
- // Build sets of valid component names and library names
4773
- const validComponentNames = new Set();
4774
- const libraryNames = new Set();
4775
- const libraryGlobalVars = new Set();
4776
- // Add dependency components
4777
- if (componentSpec?.dependencies) {
5044
+ // Build a map of dependency components to their specs
5045
+ const dependencySpecs = new Map();
5046
+ // Process embedded dependencies
5047
+ if (componentSpec?.dependencies && Array.isArray(componentSpec.dependencies)) {
4778
5048
  for (const dep of componentSpec.dependencies) {
4779
- if (dep.name) {
4780
- validComponentNames.add(dep.name);
4781
- }
4782
- }
4783
- }
4784
- // Add libraries
4785
- if (componentSpec?.libraries) {
4786
- for (const lib of componentSpec.libraries) {
4787
- if (lib.name) {
4788
- libraryNames.add(lib.name);
5049
+ if (dep && dep.name) {
5050
+ if (dep.location === 'registry') {
5051
+ const match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace, dep.registry);
5052
+ if (!match) {
5053
+ // the specified registry component was not found, we can't lint for it, but we should put a warning
5054
+ console.warn('Dependency component not found in registry', dep);
5055
+ }
5056
+ else {
5057
+ dependencySpecs.set(dep.name, match.spec);
5058
+ }
5059
+ }
5060
+ else {
5061
+ // Embedded dependencies have their spec inline
5062
+ dependencySpecs.set(dep.name, dep);
5063
+ }
4789
5064
  }
4790
- if (lib.globalVariable) {
4791
- libraryGlobalVars.add(lib.globalVariable);
5065
+ else {
5066
+ // we have an invalid dep in the spec, not a fatal error but we should log this
5067
+ console.warn(`Invalid dependency in component spec`, dep);
4792
5068
  }
4793
5069
  }
4794
5070
  }
4795
- // Check for manual destructuring from components (now optional since auto-destructuring is in place)
5071
+ // For registry dependencies, we'd need ComponentMetadataEngine
5072
+ // But since this is a static lint check, we'll focus on embedded deps
5073
+ // Registry components would need async loading which doesn't fit the current sync pattern
5074
+ // Now traverse JSX to find component usage
4796
5075
  (0, traverse_1.default)(ast, {
4797
- VariableDeclarator(path) {
4798
- // Look for: const { Something } = components;
4799
- if (t.isObjectPattern(path.node.id) &&
4800
- t.isIdentifier(path.node.init) &&
4801
- path.node.init.name === 'components') {
4802
- for (const prop of path.node.id.properties) {
4803
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
4804
- const destructuredName = prop.key.name;
4805
- // Check if this is NOT a valid component from dependencies
4806
- if (!validComponentNames.has(destructuredName)) {
4807
- // Check if it might be a library being incorrectly destructured
4808
- if (libraryNames.has(destructuredName) || libraryGlobalVars.has(destructuredName)) {
4809
- violations.push({
4810
- rule: 'invalid-components-destructuring',
4811
- severity: 'critical',
4812
- line: prop.loc?.start.line || 0,
4813
- column: prop.loc?.start.column || 0,
4814
- message: `Attempting to destructure library "${destructuredName}" from components prop. Libraries should be accessed directly via their globalVariable, not from components.`,
4815
- code: `const { ${destructuredName} } = components;`
4816
- });
5076
+ JSXElement(path) {
5077
+ const openingElement = path.node.openingElement;
5078
+ // Check if this is one of our dependency components
5079
+ if (t.isJSXIdentifier(openingElement.name)) {
5080
+ const componentName = openingElement.name.name;
5081
+ const depSpec = dependencySpecs.get(componentName);
5082
+ if (depSpec) {
5083
+ // Collect props being passed
5084
+ const passedProps = new Set();
5085
+ const passedPropNodes = new Map();
5086
+ for (const attr of openingElement.attributes) {
5087
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
5088
+ const propName = attr.name.name;
5089
+ passedProps.add(propName);
5090
+ passedPropNodes.set(propName, attr);
5091
+ }
5092
+ }
5093
+ // Check required custom props
5094
+ if (depSpec.properties && Array.isArray(depSpec.properties)) {
5095
+ const requiredProps = [];
5096
+ const optionalProps = [];
5097
+ for (const prop of depSpec.properties) {
5098
+ if (prop && prop.name && typeof prop.name === 'string') {
5099
+ if (prop.required === true) {
5100
+ requiredProps.push(prop.name);
5101
+ }
5102
+ else {
5103
+ optionalProps.push(prop.name);
5104
+ }
4817
5105
  }
4818
- else {
4819
- violations.push({
4820
- rule: 'invalid-components-destructuring',
4821
- severity: 'high',
4822
- line: prop.loc?.start.line || 0,
4823
- column: prop.loc?.start.column || 0,
4824
- 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.`,
4825
- code: `const { ${destructuredName} } = components;`
4826
- });
5106
+ }
5107
+ // Check for missing required props
5108
+ const missingRequired = requiredProps.filter(prop => {
5109
+ // Special handling for 'children' prop
5110
+ if (prop === 'children') {
5111
+ // Check if JSX element has children nodes
5112
+ const hasChildren = path.node.children && path.node.children.length > 0 &&
5113
+ path.node.children.some(child => !t.isJSXText(child) || (t.isJSXText(child) && child.value.trim() !== ''));
5114
+ return !passedProps.has(prop) && !hasChildren;
4827
5115
  }
5116
+ return !passedProps.has(prop);
5117
+ });
5118
+ // Separate children warnings from other critical props
5119
+ const missingChildren = missingRequired.filter(prop => prop === 'children');
5120
+ const missingOtherProps = missingRequired.filter(prop => prop !== 'children');
5121
+ // Critical violation for non-children required props
5122
+ if (missingOtherProps.length > 0) {
5123
+ violations.push({
5124
+ rule: 'validate-dependency-props',
5125
+ severity: 'critical',
5126
+ line: openingElement.loc?.start.line || 0,
5127
+ column: openingElement.loc?.start.column || 0,
5128
+ message: `Dependency component "${componentName}" is missing required props: ${missingOtherProps.join(', ')}. These props are marked as required in the component's specification.`,
5129
+ code: `<${componentName} ... />`
5130
+ });
4828
5131
  }
4829
- else {
4830
- // Valid component, but manual destructuring is now redundant
5132
+ // Medium severity warning for missing children when required
5133
+ if (missingChildren.length > 0) {
4831
5134
  violations.push({
4832
- rule: 'invalid-components-destructuring',
4833
- severity: 'low',
4834
- line: prop.loc?.start.line || 0,
4835
- column: prop.loc?.start.column || 0,
4836
- message: `Manual destructuring of "${destructuredName}" from components prop is redundant. Components are now auto-destructured and available directly.`,
4837
- code: `const { ${destructuredName} } = components; // Can be removed`
5135
+ rule: 'validate-dependency-props',
5136
+ severity: 'medium',
5137
+ line: openingElement.loc?.start.line || 0,
5138
+ column: openingElement.loc?.start.column || 0,
5139
+ message: `Component "${componentName}" expects children but none were provided. The 'children' prop is marked as required in the component's specification.`,
5140
+ code: `<${componentName} ... />`
4838
5141
  });
4839
5142
  }
5143
+ // Validate prop types for passed props
5144
+ for (const [propName, attrNode] of passedPropNodes) {
5145
+ const propSpec = depSpec.properties.find(p => p.name === propName);
5146
+ if (propSpec && propSpec.type) {
5147
+ const value = attrNode.value;
5148
+ // Type validation based on prop spec type
5149
+ if (propSpec.type === 'string') {
5150
+ // Check if value could be a string
5151
+ if (value && t.isJSXExpressionContainer(value)) {
5152
+ const expr = value.expression;
5153
+ // Check for obvious non-string types
5154
+ if (t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) ||
5155
+ t.isArrayExpression(expr) || (t.isObjectExpression(expr) && !t.isTemplateLiteral(expr))) {
5156
+ violations.push({
5157
+ rule: 'validate-dependency-props',
5158
+ severity: 'high',
5159
+ line: attrNode.loc?.start.line || 0,
5160
+ column: attrNode.loc?.start.column || 0,
5161
+ message: `Prop "${propName}" on component "${componentName}" expects type "string" but received a different type.`,
5162
+ code: `${propName}={...}`
5163
+ });
5164
+ }
5165
+ }
5166
+ }
5167
+ else if (propSpec.type === 'number') {
5168
+ // Check if value could be a number
5169
+ if (value && t.isJSXExpressionContainer(value)) {
5170
+ const expr = value.expression;
5171
+ if (t.isStringLiteral(expr) || t.isBooleanLiteral(expr) ||
5172
+ t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
5173
+ violations.push({
5174
+ rule: 'validate-dependency-props',
5175
+ severity: 'high',
5176
+ line: attrNode.loc?.start.line || 0,
5177
+ column: attrNode.loc?.start.column || 0,
5178
+ message: `Prop "${propName}" on component "${componentName}" expects type "number" but received a different type.`,
5179
+ code: `${propName}={...}`
5180
+ });
5181
+ }
5182
+ }
5183
+ }
5184
+ else if (propSpec.type === 'boolean') {
5185
+ // Check if value could be a boolean
5186
+ if (value && t.isJSXExpressionContainer(value)) {
5187
+ const expr = value.expression;
5188
+ if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
5189
+ t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
5190
+ violations.push({
5191
+ rule: 'validate-dependency-props',
5192
+ severity: 'high',
5193
+ line: attrNode.loc?.start.line || 0,
5194
+ column: attrNode.loc?.start.column || 0,
5195
+ message: `Prop "${propName}" on component "${componentName}" expects type "boolean" but received a different type.`,
5196
+ code: `${propName}={...}`
5197
+ });
5198
+ }
5199
+ }
5200
+ }
5201
+ else if (propSpec.type === 'array') {
5202
+ // Check if value could be an array
5203
+ if (value && t.isJSXExpressionContainer(value)) {
5204
+ const expr = value.expression;
5205
+ if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
5206
+ t.isBooleanLiteral(expr) || (t.isObjectExpression(expr) && !t.isArrayExpression(expr))) {
5207
+ violations.push({
5208
+ rule: 'validate-dependency-props',
5209
+ severity: 'high',
5210
+ line: attrNode.loc?.start.line || 0,
5211
+ column: attrNode.loc?.start.column || 0,
5212
+ message: `Prop "${propName}" on component "${componentName}" expects type "array" but received a different type.`,
5213
+ code: `${propName}={...}`
5214
+ });
5215
+ }
5216
+ }
5217
+ }
5218
+ else if (propSpec.type === 'object') {
5219
+ // Check if value could be an object
5220
+ if (value && t.isJSXExpressionContainer(value)) {
5221
+ const expr = value.expression;
5222
+ if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
5223
+ t.isBooleanLiteral(expr) || t.isArrayExpression(expr)) {
5224
+ violations.push({
5225
+ rule: 'validate-dependency-props',
5226
+ severity: 'high',
5227
+ line: attrNode.loc?.start.line || 0,
5228
+ column: attrNode.loc?.start.column || 0,
5229
+ message: `Prop "${propName}" on component "${componentName}" expects type "object" but received a different type.`,
5230
+ code: `${propName}={...}`
5231
+ });
5232
+ }
5233
+ }
5234
+ }
5235
+ }
5236
+ }
5237
+ // Check for unknown props (props not in the spec)
5238
+ const specPropNames = new Set(depSpec.properties.map(p => p.name).filter(Boolean));
5239
+ const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
5240
+ const reactSpecialProps = new Set(['children']);
5241
+ for (const passedProp of passedProps) {
5242
+ if (!specPropNames.has(passedProp) && !standardProps.has(passedProp) && !reactSpecialProps.has(passedProp)) {
5243
+ violations.push({
5244
+ rule: 'validate-dependency-props',
5245
+ severity: 'medium',
5246
+ line: passedPropNodes.get(passedProp)?.loc?.start.line || 0,
5247
+ column: passedPropNodes.get(passedProp)?.loc?.start.column || 0,
5248
+ message: `Prop "${passedProp}" is not defined in the specification for component "${componentName}". In addition to the standard MJ props, valid custom props: ${Array.from(specPropNames).join(', ') || 'none'}.`,
5249
+ code: `${passedProp}={...}`
5250
+ });
5251
+ }
5252
+ }
4840
5253
  }
4841
5254
  }
4842
5255
  }
@@ -5238,14 +5651,14 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5238
5651
  });
5239
5652
  }
5240
5653
  } else if (componentsFromProp.has(tagName)) {
5241
- // This shouldn't happen since dependency components are auto-destructured
5242
- // 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
5243
5656
  violations.push({
5244
5657
  rule: 'undefined-jsx-component',
5245
5658
  severity: 'high',
5246
5659
  line: openingElement.loc?.start.line || 0,
5247
5660
  column: openingElement.loc?.start.column || 0,
5248
- 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;`,
5249
5662
  code: `<${tagName} ... />`
5250
5663
  });
5251
5664
  } else {
@@ -6903,10 +7316,10 @@ Correct pattern:
6903
7316
  if (!isUsed) {
6904
7317
  violations.push({
6905
7318
  rule: 'unused-component-dependencies',
6906
- severity: 'high',
7319
+ severity: 'low',
6907
7320
  line: 1,
6908
7321
  column: 0,
6909
- message: `Component dependency "${depName}" is declared but never used. This likely means missing functionality.`,
7322
+ message: `Component dependency "${depName}" is declared but never used. Consider removing it if not needed.`,
6910
7323
  code: `Expected usage: <${depName} /> or <components.${depName} />`
6911
7324
  });
6912
7325
  }
@@ -7054,91 +7467,592 @@ Correct pattern:
7054
7467
  appliesTo: 'all',
7055
7468
  test: (ast, componentName) => {
7056
7469
  const violations = [];
7057
- // Count all function declarations and expressions at the top level
7058
- const functionDeclarations = [];
7059
- const functionExpressions = [];
7060
- (0, traverse_1.default)(ast, {
7061
- FunctionDeclaration(path) {
7062
- // Only check top-level functions (not nested inside other functions)
7063
- const parent = path.getFunctionParent();
7064
- if (!parent) {
7065
- const funcName = path.node.id?.name || 'anonymous';
7066
- functionDeclarations.push({
7067
- name: funcName,
7068
- line: path.node.loc?.start.line || 0,
7069
- column: path.node.loc?.start.column || 0
7070
- });
7071
- }
7072
- },
7073
- VariableDeclaration(path) {
7074
- // Check for const/let/var func = function() or arrow functions at top level
7075
- const parent = path.getFunctionParent();
7076
- if (!parent) {
7077
- for (const declarator of path.node.declarations) {
7078
- if (t.isVariableDeclarator(declarator) &&
7079
- (t.isFunctionExpression(declarator.init) ||
7080
- t.isArrowFunctionExpression(declarator.init))) {
7081
- const funcName = t.isIdentifier(declarator.id) ? declarator.id.name : 'anonymous';
7082
- functionExpressions.push({
7083
- name: funcName,
7084
- line: declarator.loc?.start.line || 0,
7085
- column: declarator.loc?.start.column || 0
7086
- });
7087
- }
7088
- }
7089
- }
7090
- }
7091
- });
7092
- const allFunctions = [...functionDeclarations, ...functionExpressions];
7093
- // Check if we have more than one function
7094
- if (allFunctions.length > 1) {
7095
- // Find which one is the main component
7096
- const mainComponentIndex = allFunctions.findIndex(f => f.name === componentName);
7097
- 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
7098
7486
  violations.push({
7099
7487
  rule: 'single-function-only',
7100
7488
  severity: 'critical',
7101
- line: otherFunctions[0].line,
7102
- column: otherFunctions[0].column,
7103
- 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.`,
7104
- 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}(...) { ... }`
7105
7493
  });
7106
- // Add a violation for each extra function
7107
- 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
+ }
7108
7507
  violations.push({
7109
7508
  rule: 'single-function-only',
7110
7509
  severity: 'critical',
7111
- line: func.line,
7112
- column: func.column,
7113
- message: `Extra function "${func.name}" not allowed. Each component must be a single function. Move this to a separate component dependency.`,
7114
- 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: ''
7115
7514
  });
7116
7515
  }
7117
7516
  }
7118
- // Also check that the single function matches the component name
7119
- 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
+ }
7120
7540
  violations.push({
7121
7541
  rule: 'single-function-only',
7122
7542
  severity: 'critical',
7123
- line: allFunctions[0].line,
7124
- column: allFunctions[0].column,
7125
- message: `Component function name "${allFunctions[0].name}" does not match component name "${componentName}". The function must be named exactly as specified.`,
7126
- 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: ''
7127
7547
  });
7548
+ // Don't check name if it's not a function declaration
7549
+ return violations;
7128
7550
  }
7129
- // Check for no function at all
7130
- if (allFunctions.length === 0) {
7551
+ // Check that the function name matches the component name
7552
+ const functionName = firstStatement.id?.name;
7553
+ if (functionName !== componentName) {
7131
7554
  violations.push({
7132
7555
  rule: 'single-function-only',
7133
7556
  severity: 'critical',
7134
- line: 1,
7135
- column: 0,
7136
- message: `Component code must contain exactly one function named "${componentName}". No functions found.`,
7137
- 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
+ }
7138
7581
  });
7139
7582
  }
7140
7583
  return violations;
7141
7584
  }
7585
+ },
7586
+ // New rules for catching RunQuery/RunView result access patterns
7587
+ {
7588
+ name: 'runquery-runview-ternary-array-check',
7589
+ appliesTo: 'all',
7590
+ test: (ast, componentName, componentSpec) => {
7591
+ const violations = [];
7592
+ // Track variables that hold RunView/RunQuery results
7593
+ const resultVariables = new Map();
7594
+ // First pass: identify all RunView/RunQuery calls and their assigned variables
7595
+ (0, traverse_1.default)(ast, {
7596
+ AwaitExpression(path) {
7597
+ const callExpr = path.node.argument;
7598
+ if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
7599
+ const callee = callExpr.callee;
7600
+ // Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
7601
+ if (t.isMemberExpression(callee.object) &&
7602
+ t.isIdentifier(callee.object.object) &&
7603
+ callee.object.object.name === 'utilities' &&
7604
+ t.isIdentifier(callee.object.property)) {
7605
+ const subObject = callee.object.property.name;
7606
+ const method = t.isIdentifier(callee.property) ? callee.property.name : '';
7607
+ let methodType = null;
7608
+ if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
7609
+ methodType = method;
7610
+ }
7611
+ else if (subObject === 'rq' && method === 'RunQuery') {
7612
+ methodType = 'RunQuery';
7613
+ }
7614
+ if (methodType) {
7615
+ // Check if this is being assigned to a variable
7616
+ const parent = path.parent;
7617
+ if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
7618
+ // const result = await utilities.rv.RunView(...)
7619
+ resultVariables.set(parent.id.name, {
7620
+ line: parent.id.loc?.start.line || 0,
7621
+ column: parent.id.loc?.start.column || 0,
7622
+ method: methodType,
7623
+ varName: parent.id.name
7624
+ });
7625
+ }
7626
+ else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
7627
+ // result = await utilities.rv.RunView(...)
7628
+ resultVariables.set(parent.left.name, {
7629
+ line: parent.left.loc?.start.line || 0,
7630
+ column: parent.left.loc?.start.column || 0,
7631
+ method: methodType,
7632
+ varName: parent.left.name
7633
+ });
7634
+ }
7635
+ }
7636
+ }
7637
+ }
7638
+ }
7639
+ });
7640
+ // Second pass: check for Array.isArray(result) ? result : [] pattern
7641
+ (0, traverse_1.default)(ast, {
7642
+ ConditionalExpression(path) {
7643
+ const test = path.node.test;
7644
+ const consequent = path.node.consequent;
7645
+ const alternate = path.node.alternate;
7646
+ // Check for Array.isArray(variable) pattern
7647
+ if (t.isCallExpression(test) &&
7648
+ t.isMemberExpression(test.callee) &&
7649
+ t.isIdentifier(test.callee.object) &&
7650
+ test.callee.object.name === 'Array' &&
7651
+ t.isIdentifier(test.callee.property) &&
7652
+ test.callee.property.name === 'isArray' &&
7653
+ test.arguments.length === 1 &&
7654
+ t.isIdentifier(test.arguments[0])) {
7655
+ const varName = test.arguments[0].name;
7656
+ // Check if this variable is a RunQuery/RunView result
7657
+ if (resultVariables.has(varName)) {
7658
+ const resultInfo = resultVariables.get(varName);
7659
+ // Check if the consequent is the same variable and alternate is []
7660
+ if (t.isIdentifier(consequent) &&
7661
+ consequent.name === varName &&
7662
+ t.isArrayExpression(alternate) &&
7663
+ alternate.elements.length === 0) {
7664
+ violations.push({
7665
+ rule: 'runquery-runview-ternary-array-check',
7666
+ severity: 'critical',
7667
+ line: test.loc?.start.line || 0,
7668
+ column: test.loc?.start.column || 0,
7669
+ message: `${resultInfo.method} never returns an array directly. The pattern "Array.isArray(${varName}) ? ${varName} : []" will always evaluate to [] because ${varName} is an object with { Success, Results, ErrorMessage }.
7670
+
7671
+ Correct patterns:
7672
+ // Option 1: Simple with fallback
7673
+ ${varName}.Results || []
7674
+
7675
+ // Option 2: Check success first
7676
+ if (${varName}.Success) {
7677
+ setData(${varName}.Results || []);
7678
+ } else {
7679
+ console.error('Failed:', ${varName}.ErrorMessage);
7680
+ setData([]);
7681
+ }`,
7682
+ code: `Array.isArray(${varName}) ? ${varName} : []`
7683
+ });
7684
+ }
7685
+ }
7686
+ }
7687
+ }
7688
+ });
7689
+ return violations;
7690
+ }
7691
+ },
7692
+ {
7693
+ name: 'runquery-runview-direct-setstate',
7694
+ appliesTo: 'all',
7695
+ test: (ast, componentName, componentSpec) => {
7696
+ const violations = [];
7697
+ // Track variables that hold RunView/RunQuery results
7698
+ const resultVariables = new Map();
7699
+ // First pass: identify all RunView/RunQuery calls and their assigned variables
7700
+ (0, traverse_1.default)(ast, {
7701
+ AwaitExpression(path) {
7702
+ const callExpr = path.node.argument;
7703
+ if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
7704
+ const callee = callExpr.callee;
7705
+ // Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
7706
+ if (t.isMemberExpression(callee.object) &&
7707
+ t.isIdentifier(callee.object.object) &&
7708
+ callee.object.object.name === 'utilities' &&
7709
+ t.isIdentifier(callee.object.property)) {
7710
+ const subObject = callee.object.property.name;
7711
+ const method = t.isIdentifier(callee.property) ? callee.property.name : '';
7712
+ let methodType = null;
7713
+ if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
7714
+ methodType = method;
7715
+ }
7716
+ else if (subObject === 'rq' && method === 'RunQuery') {
7717
+ methodType = 'RunQuery';
7718
+ }
7719
+ if (methodType) {
7720
+ // Check if this is being assigned to a variable
7721
+ const parent = path.parent;
7722
+ if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
7723
+ resultVariables.set(parent.id.name, {
7724
+ line: parent.id.loc?.start.line || 0,
7725
+ column: parent.id.loc?.start.column || 0,
7726
+ method: methodType,
7727
+ varName: parent.id.name
7728
+ });
7729
+ }
7730
+ else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
7731
+ resultVariables.set(parent.left.name, {
7732
+ line: parent.left.loc?.start.line || 0,
7733
+ column: parent.left.loc?.start.column || 0,
7734
+ method: methodType,
7735
+ varName: parent.left.name
7736
+ });
7737
+ }
7738
+ }
7739
+ }
7740
+ }
7741
+ }
7742
+ });
7743
+ // Second pass: check for passing result directly to setState functions
7744
+ (0, traverse_1.default)(ast, {
7745
+ CallExpression(path) {
7746
+ const callee = path.node.callee;
7747
+ // Check if this is a setState function call
7748
+ if (t.isIdentifier(callee)) {
7749
+ const funcName = callee.name;
7750
+ // Common setState patterns
7751
+ const setStatePatterns = [
7752
+ /^set[A-Z]/, // setData, setChartData, setItems, etc.
7753
+ /^update[A-Z]/, // updateData, updateItems, etc.
7754
+ ];
7755
+ const isSetStateFunction = setStatePatterns.some(pattern => pattern.test(funcName));
7756
+ if (isSetStateFunction && path.node.arguments.length > 0) {
7757
+ const firstArg = path.node.arguments[0];
7758
+ // Check if the argument is a ternary with Array.isArray check
7759
+ if (t.isConditionalExpression(firstArg)) {
7760
+ const test = firstArg.test;
7761
+ const consequent = firstArg.consequent;
7762
+ const alternate = firstArg.alternate;
7763
+ // Check for Array.isArray(variable) ? variable : []
7764
+ if (t.isCallExpression(test) &&
7765
+ t.isMemberExpression(test.callee) &&
7766
+ t.isIdentifier(test.callee.object) &&
7767
+ test.callee.object.name === 'Array' &&
7768
+ t.isIdentifier(test.callee.property) &&
7769
+ test.callee.property.name === 'isArray' &&
7770
+ test.arguments.length === 1 &&
7771
+ t.isIdentifier(test.arguments[0])) {
7772
+ const varName = test.arguments[0].name;
7773
+ if (resultVariables.has(varName) &&
7774
+ t.isIdentifier(consequent) &&
7775
+ consequent.name === varName) {
7776
+ const resultInfo = resultVariables.get(varName);
7777
+ violations.push({
7778
+ rule: 'runquery-runview-direct-setstate',
7779
+ severity: 'critical',
7780
+ line: firstArg.loc?.start.line || 0,
7781
+ column: firstArg.loc?.start.column || 0,
7782
+ message: `Passing ${resultInfo.method} result with incorrect Array.isArray check to ${funcName}. This will always pass an empty array because ${resultInfo.method} returns an object, not an array.
7783
+
7784
+ Correct pattern:
7785
+ if (${varName}.Success) {
7786
+ ${funcName}(${varName}.Results || []);
7787
+ } else {
7788
+ console.error('Failed to load data:', ${varName}.ErrorMessage);
7789
+ ${funcName}([]);
7790
+ }
7791
+
7792
+ // Or simpler:
7793
+ ${funcName}(${varName}.Results || []);`,
7794
+ code: `${funcName}(Array.isArray(${varName}) ? ${varName} : [])`
7795
+ });
7796
+ }
7797
+ }
7798
+ }
7799
+ // Check if passing result directly (not accessing .Results)
7800
+ if (t.isIdentifier(firstArg) && resultVariables.has(firstArg.name)) {
7801
+ const resultInfo = resultVariables.get(firstArg.name);
7802
+ violations.push({
7803
+ rule: 'runquery-runview-direct-setstate',
7804
+ severity: 'critical',
7805
+ line: firstArg.loc?.start.line || 0,
7806
+ column: firstArg.loc?.start.column || 0,
7807
+ message: `Passing ${resultInfo.method} result object directly to ${funcName}. The result is an object { Success, Results, ErrorMessage }, not the data array.
7808
+
7809
+ Correct pattern:
7810
+ if (${firstArg.name}.Success) {
7811
+ ${funcName}(${firstArg.name}.Results || []);
7812
+ } else {
7813
+ console.error('Failed to load data:', ${firstArg.name}.ErrorMessage);
7814
+ ${funcName}([]);
7815
+ }`,
7816
+ code: `${funcName}(${firstArg.name})`
7817
+ });
7818
+ }
7819
+ }
7820
+ }
7821
+ }
7822
+ });
7823
+ return violations;
7824
+ }
7825
+ },
7826
+ {
7827
+ name: 'styles-invalid-path',
7828
+ appliesTo: 'all',
7829
+ test: (ast, componentName, componentSpec) => {
7830
+ const violations = [];
7831
+ const analyzer = ComponentLinter.getStylesAnalyzer();
7832
+ (0, traverse_1.default)(ast, {
7833
+ MemberExpression(path) {
7834
+ // Build the complete property chain first
7835
+ let propertyChain = [];
7836
+ let current = path.node;
7837
+ // Walk up from the deepest member expression to build the full chain
7838
+ while (t.isMemberExpression(current)) {
7839
+ if (t.isIdentifier(current.property)) {
7840
+ propertyChain.unshift(current.property.name);
7841
+ }
7842
+ if (t.isIdentifier(current.object)) {
7843
+ propertyChain.unshift(current.object.name);
7844
+ break;
7845
+ }
7846
+ current = current.object;
7847
+ }
7848
+ // Only process if this is a styles access
7849
+ if (propertyChain[0] === 'styles') {
7850
+ // Validate the path
7851
+ if (!analyzer.isValidPath(propertyChain)) {
7852
+ const suggestions = analyzer.getSuggestionsForPath(propertyChain);
7853
+ const accessPath = propertyChain.join('.');
7854
+ let message = `Invalid styles property path: "${accessPath}"`;
7855
+ if (suggestions.didYouMean) {
7856
+ message += `\n\nDid you mean: ${suggestions.didYouMean}?`;
7857
+ }
7858
+ if (suggestions.correctPaths.length > 0) {
7859
+ message += `\n\nThe property "${propertyChain[propertyChain.length - 1]}" exists at:`;
7860
+ suggestions.correctPaths.forEach((p) => {
7861
+ message += `\n - ${p}`;
7862
+ });
7863
+ }
7864
+ if (suggestions.availableAtParent.length > 0) {
7865
+ const parentPath = propertyChain.slice(0, -1).join('.');
7866
+ message += `\n\nAvailable properties at ${parentPath}:`;
7867
+ message += `\n ${suggestions.availableAtParent.slice(0, 5).join(', ')}`;
7868
+ if (suggestions.availableAtParent.length > 5) {
7869
+ message += ` (and ${suggestions.availableAtParent.length - 5} more)`;
7870
+ }
7871
+ }
7872
+ // Get a contextual default value
7873
+ const defaultValue = analyzer.getDefaultValueForPath(propertyChain);
7874
+ message += `\n\nSuggested fix with safe access:\n ${accessPath.replace(/\./g, '?.')} || ${defaultValue}`;
7875
+ violations.push({
7876
+ rule: 'styles-invalid-path',
7877
+ severity: 'critical',
7878
+ line: path.node.loc?.start.line || 0,
7879
+ column: path.node.loc?.start.column || 0,
7880
+ message: message,
7881
+ code: accessPath
7882
+ });
7883
+ }
7884
+ }
7885
+ }
7886
+ });
7887
+ return violations;
7888
+ }
7889
+ },
7890
+ {
7891
+ name: 'styles-unsafe-access',
7892
+ appliesTo: 'all',
7893
+ test: (ast, componentName, componentSpec) => {
7894
+ const violations = [];
7895
+ const analyzer = ComponentLinter.getStylesAnalyzer();
7896
+ (0, traverse_1.default)(ast, {
7897
+ MemberExpression(path) {
7898
+ // Build the complete property chain first
7899
+ let propertyChain = [];
7900
+ let current = path.node;
7901
+ let hasOptionalChaining = path.node.optional || false;
7902
+ // Walk up from the deepest member expression to build the full chain
7903
+ while (t.isMemberExpression(current)) {
7904
+ if (current.optional) {
7905
+ hasOptionalChaining = true;
7906
+ }
7907
+ if (t.isIdentifier(current.property)) {
7908
+ propertyChain.unshift(current.property.name);
7909
+ }
7910
+ if (t.isIdentifier(current.object)) {
7911
+ propertyChain.unshift(current.object.name);
7912
+ break;
7913
+ }
7914
+ current = current.object;
7915
+ }
7916
+ // Only process if this is a styles access
7917
+ if (propertyChain[0] === 'styles') {
7918
+ // Only check valid paths for safe access
7919
+ if (analyzer.isValidPath(propertyChain)) {
7920
+ // Check if this is a nested access without optional chaining or fallback
7921
+ if (propertyChain.length > 2 && !hasOptionalChaining) {
7922
+ // Check if there's a fallback (|| operator)
7923
+ const parent = path.parent;
7924
+ const hasFallback = t.isLogicalExpression(parent) && parent.operator === '||';
7925
+ if (!hasFallback) {
7926
+ const accessPath = propertyChain.join('.');
7927
+ const defaultValue = analyzer.getDefaultValueForPath(propertyChain);
7928
+ violations.push({
7929
+ rule: 'styles-unsafe-access',
7930
+ severity: 'high',
7931
+ line: path.node.loc?.start.line || 0,
7932
+ column: path.node.loc?.start.column || 0,
7933
+ message: `Unsafe styles property access: "${accessPath}". While this path is valid, you should use optional chaining for safety.
7934
+
7935
+ Example with optional chaining:
7936
+ ${accessPath.replace(/\./g, '?.')} || ${defaultValue}
7937
+
7938
+ This prevents runtime errors if the styles object structure changes.`,
7939
+ code: accessPath
7940
+ });
7941
+ }
7942
+ }
7943
+ }
7944
+ }
7945
+ }
7946
+ });
7947
+ return violations;
7948
+ }
7949
+ },
7950
+ {
7951
+ name: 'runquery-runview-spread-operator',
7952
+ appliesTo: 'all',
7953
+ test: (ast, componentName, componentSpec) => {
7954
+ const violations = [];
7955
+ // Track variables that hold RunView/RunQuery results
7956
+ const resultVariables = new Map();
7957
+ // First pass: identify all RunView/RunQuery calls
7958
+ (0, traverse_1.default)(ast, {
7959
+ AwaitExpression(path) {
7960
+ const callExpr = path.node.argument;
7961
+ if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
7962
+ const callee = callExpr.callee;
7963
+ if (t.isMemberExpression(callee.object) &&
7964
+ t.isIdentifier(callee.object.object) &&
7965
+ callee.object.object.name === 'utilities' &&
7966
+ t.isIdentifier(callee.object.property)) {
7967
+ const subObject = callee.object.property.name;
7968
+ const method = t.isIdentifier(callee.property) ? callee.property.name : '';
7969
+ let methodType = null;
7970
+ if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
7971
+ methodType = method;
7972
+ }
7973
+ else if (subObject === 'rq' && method === 'RunQuery') {
7974
+ methodType = 'RunQuery';
7975
+ }
7976
+ if (methodType) {
7977
+ const parent = path.parent;
7978
+ if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
7979
+ resultVariables.set(parent.id.name, {
7980
+ line: parent.id.loc?.start.line || 0,
7981
+ column: parent.id.loc?.start.column || 0,
7982
+ method: methodType,
7983
+ varName: parent.id.name
7984
+ });
7985
+ }
7986
+ }
7987
+ }
7988
+ }
7989
+ }
7990
+ });
7991
+ // Second pass: check for spread operator usage
7992
+ (0, traverse_1.default)(ast, {
7993
+ SpreadElement(path) {
7994
+ if (t.isIdentifier(path.node.argument)) {
7995
+ const varName = path.node.argument.name;
7996
+ if (resultVariables.has(varName)) {
7997
+ const resultInfo = resultVariables.get(varName);
7998
+ violations.push({
7999
+ rule: 'runquery-runview-spread-operator',
8000
+ severity: 'critical',
8001
+ line: path.node.loc?.start.line || 0,
8002
+ column: path.node.loc?.start.column || 0,
8003
+ message: `Cannot use spread operator on ${resultInfo.method} result object. Use ...${varName}.Results to spread the data array.
8004
+
8005
+ Correct pattern:
8006
+ const allData = [...existingData, ...${varName}.Results];
8007
+
8008
+ // Or with null safety:
8009
+ const allData = [...existingData, ...(${varName}.Results || [])];`,
8010
+ code: `...${varName}`
8011
+ });
8012
+ }
8013
+ }
8014
+ }
8015
+ });
8016
+ return violations;
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
+ }
7142
8056
  }
7143
8057
  ];
7144
8058
  //# sourceMappingURL=component-linter.js.map