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