@memberjunction/react-test-harness 2.96.0 → 2.98.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 +386 -351
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +1 -4
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +404 -79
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/library-lint-cache.d.ts +4 -0
- package/dist/lib/library-lint-cache.d.ts.map +1 -1
- package/dist/lib/library-lint-cache.js +61 -3
- package/dist/lib/library-lint-cache.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 +6 -4
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -196,6 +196,10 @@ class ComponentLinter {
|
|
|
196
196
|
}
|
|
197
197
|
static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode, options) {
|
|
198
198
|
try {
|
|
199
|
+
// Require contextUser when libraries need to be checked
|
|
200
|
+
if (componentSpec?.libraries && componentSpec.libraries.length > 0 && !contextUser) {
|
|
201
|
+
throw new Error('contextUser is required when linting components with library dependencies. This is needed to load library-specific lint rules from the database.');
|
|
202
|
+
}
|
|
199
203
|
// Parse with error recovery to get both AST and errors
|
|
200
204
|
const parseResult = parser.parse(code, {
|
|
201
205
|
sourceType: 'module',
|
|
@@ -222,14 +226,16 @@ class ComponentLinter {
|
|
|
222
226
|
}
|
|
223
227
|
// If we have critical syntax errors, return immediately with those
|
|
224
228
|
if (syntaxViolations.length > 0) {
|
|
229
|
+
// Add suggestions directly to syntax violations
|
|
230
|
+
this.generateSyntaxErrorSuggestions(syntaxViolations);
|
|
225
231
|
return {
|
|
226
232
|
success: false,
|
|
227
233
|
violations: syntaxViolations,
|
|
228
|
-
suggestions: this.generateSyntaxErrorSuggestions(syntaxViolations),
|
|
229
234
|
criticalCount: syntaxViolations.length,
|
|
230
235
|
highCount: 0,
|
|
231
236
|
mediumCount: 0,
|
|
232
|
-
lowCount: 0
|
|
237
|
+
lowCount: 0,
|
|
238
|
+
hasErrors: true
|
|
233
239
|
};
|
|
234
240
|
}
|
|
235
241
|
// Continue with existing linting logic
|
|
@@ -257,7 +263,7 @@ class ComponentLinter {
|
|
|
257
263
|
violations.push(...dataViolations);
|
|
258
264
|
}
|
|
259
265
|
// Apply library-specific lint rules if available
|
|
260
|
-
if (componentSpec?.libraries
|
|
266
|
+
if (componentSpec?.libraries) {
|
|
261
267
|
const libraryViolations = await this.applyLibraryLintRules(ast, componentSpec, contextUser, debugMode);
|
|
262
268
|
violations.push(...libraryViolations);
|
|
263
269
|
}
|
|
@@ -299,16 +305,16 @@ class ComponentLinter {
|
|
|
299
305
|
}
|
|
300
306
|
console.log('');
|
|
301
307
|
}
|
|
302
|
-
//
|
|
303
|
-
|
|
308
|
+
// Add suggestions directly to violations
|
|
309
|
+
this.addSuggestionsToViolations(uniqueViolations);
|
|
304
310
|
return {
|
|
305
311
|
success: criticalCount === 0 && highCount === 0, // Only fail on critical/high
|
|
306
312
|
violations: uniqueViolations,
|
|
307
|
-
suggestions,
|
|
308
313
|
criticalCount,
|
|
309
314
|
highCount,
|
|
310
315
|
mediumCount,
|
|
311
|
-
lowCount
|
|
316
|
+
lowCount,
|
|
317
|
+
hasErrors: criticalCount > 0 || highCount > 0
|
|
312
318
|
};
|
|
313
319
|
}
|
|
314
320
|
catch (error) {
|
|
@@ -322,7 +328,7 @@ class ComponentLinter {
|
|
|
322
328
|
column: 0,
|
|
323
329
|
message: `Failed to parse component: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
324
330
|
}],
|
|
325
|
-
|
|
331
|
+
hasErrors: true
|
|
326
332
|
};
|
|
327
333
|
}
|
|
328
334
|
}
|
|
@@ -690,14 +696,17 @@ class ComponentLinter {
|
|
|
690
696
|
});
|
|
691
697
|
return unique;
|
|
692
698
|
}
|
|
693
|
-
|
|
694
|
-
|
|
699
|
+
/**
|
|
700
|
+
* Adds suggestions directly to violations based on their rule type
|
|
701
|
+
* @param violations Array of violations to enhance with suggestions
|
|
702
|
+
* @returns The same violations array with suggestions embedded
|
|
703
|
+
*/
|
|
704
|
+
static addSuggestionsToViolations(violations) {
|
|
695
705
|
for (const violation of violations) {
|
|
696
706
|
switch (violation.rule) {
|
|
697
707
|
case 'no-import-statements':
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
suggestion: 'Remove all import statements. Interactive components receive everything through props.',
|
|
708
|
+
violation.suggestion = {
|
|
709
|
+
text: 'Remove all import statements. Interactive components receive everything through props.',
|
|
701
710
|
example: `// ❌ WRONG - Using import statements:
|
|
702
711
|
import React from 'react';
|
|
703
712
|
import { useState } from 'react';
|
|
@@ -725,12 +734,11 @@ function MyComponent({ utilities, styles, components }) {
|
|
|
725
734
|
// 2. Passed through the 'components' prop (child components)
|
|
726
735
|
// 3. Passed through the 'styles' prop (styling)
|
|
727
736
|
// 4. Available globally (React hooks)`
|
|
728
|
-
}
|
|
737
|
+
};
|
|
729
738
|
break;
|
|
730
739
|
case 'no-export-statements':
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
suggestion: 'Remove all export statements. The component function should be the only code, not exported.',
|
|
740
|
+
violation.suggestion = {
|
|
741
|
+
text: 'Remove all export statements. The component function should be the only code, not exported.',
|
|
734
742
|
example: `// ❌ WRONG - Using export:
|
|
735
743
|
export function MyComponent({ utilities }) {
|
|
736
744
|
return <div>Hello</div>;
|
|
@@ -752,12 +760,11 @@ function MyComponent({ utilities, styles, components }) {
|
|
|
752
760
|
// The component is self-contained.
|
|
753
761
|
// No exports needed - the host environment
|
|
754
762
|
// will execute the function directly.`
|
|
755
|
-
}
|
|
763
|
+
};
|
|
756
764
|
break;
|
|
757
765
|
case 'no-require-statements':
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
suggestion: 'Remove all require() and dynamic import() statements. Use props instead.',
|
|
766
|
+
violation.suggestion = {
|
|
767
|
+
text: 'Remove all require() and dynamic import() statements. Use props instead.',
|
|
761
768
|
example: `// ❌ WRONG - Using require or dynamic import:
|
|
762
769
|
function MyComponent({ utilities }) {
|
|
763
770
|
const lodash = require('lodash');
|
|
@@ -788,12 +795,11 @@ function MyComponent({ utilities, styles, components }) {
|
|
|
788
795
|
// - Passed via props (utilities, components, styles)
|
|
789
796
|
// - Available globally (React hooks)
|
|
790
797
|
// No module loading allowed!`
|
|
791
|
-
}
|
|
798
|
+
};
|
|
792
799
|
break;
|
|
793
800
|
case 'use-function-declaration':
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
suggestion: 'Use function declaration syntax for TOP-LEVEL component definitions. Arrow functions are fine inside components.',
|
|
801
|
+
violation.suggestion = {
|
|
802
|
+
text: 'Use function declaration syntax for TOP-LEVEL component definitions. Arrow functions are fine inside components.',
|
|
797
803
|
example: `// ❌ WRONG - Top-level arrow function component:
|
|
798
804
|
const MyComponent = ({ utilities, styles, components }) => {
|
|
799
805
|
const [state, setState] = useState('');
|
|
@@ -826,12 +832,11 @@ function ChildComponent() {
|
|
|
826
832
|
// 3. Hoisting allows flexible code organization
|
|
827
833
|
// 4. Consistent with React documentation patterns
|
|
828
834
|
// 5. Easier to distinguish from regular variables`
|
|
829
|
-
}
|
|
835
|
+
};
|
|
830
836
|
break;
|
|
831
837
|
case 'no-return-component':
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
suggestion: 'Remove the return statement at the end of the file. The component function should stand alone.',
|
|
838
|
+
violation.suggestion = {
|
|
839
|
+
text: 'Remove the return statement at the end of the file. The component function should stand alone.',
|
|
835
840
|
example: `// ❌ WRONG - Returning the component:
|
|
836
841
|
function MyComponent({ utilities, styles, components }) {
|
|
837
842
|
const [state, setState] = useState('');
|
|
@@ -858,12 +863,11 @@ function MyComponent({ utilities, styles, components }) {
|
|
|
858
863
|
|
|
859
864
|
// The runtime will find and execute your component
|
|
860
865
|
// by its function name. No need to return or reference it!`
|
|
861
|
-
}
|
|
866
|
+
};
|
|
862
867
|
break;
|
|
863
868
|
case 'no-iife-wrapper':
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
suggestion: 'Remove the IIFE wrapper. Component code should be plain functions, not wrapped in immediately invoked functions.',
|
|
869
|
+
violation.suggestion = {
|
|
870
|
+
text: 'Remove the IIFE wrapper. Component code should be plain functions, not wrapped in immediately invoked functions.',
|
|
867
871
|
example: `// ❌ WRONG - IIFE wrapper patterns:
|
|
868
872
|
(function() {
|
|
869
873
|
function MyComponent({ utilities, styles, components }) {
|
|
@@ -897,12 +901,11 @@ function MyComponent({ utilities, styles, components }) {
|
|
|
897
901
|
// 3. IIFEs prevent proper component discovery
|
|
898
902
|
// 4. Makes debugging harder
|
|
899
903
|
// 5. Unnecessary complexity`
|
|
900
|
-
}
|
|
904
|
+
};
|
|
901
905
|
break;
|
|
902
906
|
case 'full-state-ownership':
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
suggestion: 'Components must manage ALL their own state internally. Use proper naming conventions for initialization.',
|
|
907
|
+
violation.suggestion = {
|
|
908
|
+
text: 'Components must manage ALL their own state internally. Use proper naming conventions for initialization.',
|
|
906
909
|
example: `// ❌ WRONG - Controlled state props:
|
|
907
910
|
function PaginationControls({ currentPage, filters, sortBy, onPageChange }) {
|
|
908
911
|
// These props suggest parent controls the state - WRONG!
|
|
@@ -962,12 +965,11 @@ function DataTable({
|
|
|
962
965
|
// - Direct state names (currentPage, selectedId, activeTab)
|
|
963
966
|
// - State without 'initial'/'default' prefix (sortBy, filters, searchTerm)
|
|
964
967
|
// - Controlled patterns (value + onChange, checked + onChange)`
|
|
965
|
-
}
|
|
968
|
+
};
|
|
966
969
|
break;
|
|
967
970
|
case 'no-use-reducer':
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
suggestion: 'Use useState for state management, not useReducer',
|
|
971
|
+
violation.suggestion = {
|
|
972
|
+
text: 'Use useState for state management, not useReducer',
|
|
971
973
|
example: `// Instead of:
|
|
972
974
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
973
975
|
|
|
@@ -990,12 +992,11 @@ function Component({ savedUserSettings, onSaveUserSettings }) {
|
|
|
990
992
|
}
|
|
991
993
|
};
|
|
992
994
|
}`
|
|
993
|
-
}
|
|
995
|
+
};
|
|
994
996
|
break;
|
|
995
997
|
case 'no-data-prop':
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
suggestion: 'Replace generic data prop with specific named props',
|
|
998
|
+
violation.suggestion = {
|
|
999
|
+
text: 'Replace generic data prop with specific named props',
|
|
999
1000
|
example: `// Instead of:
|
|
1000
1001
|
function Component({ data, savedUserSettings, onSaveUserSettings }) {
|
|
1001
1002
|
return <div>{data.items.map(...)}</div>;
|
|
@@ -1013,12 +1014,11 @@ function Component({ items, customers, savedUserSettings, onSaveUserSettings })
|
|
|
1013
1014
|
|
|
1014
1015
|
// Load data using utilities:
|
|
1015
1016
|
const result = await utilities.rv.RunView({ entityName: 'Items' });`
|
|
1016
|
-
}
|
|
1017
|
+
};
|
|
1017
1018
|
break;
|
|
1018
1019
|
case 'saved-user-settings-pattern':
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
suggestion: 'Only save important user preferences, not ephemeral UI state',
|
|
1020
|
+
violation.suggestion = {
|
|
1021
|
+
text: 'Only save important user preferences, not ephemeral UI state',
|
|
1022
1022
|
example: `// ✅ SAVE these (important preferences):
|
|
1023
1023
|
- Selected items/tabs: selectedCustomerId, activeTab
|
|
1024
1024
|
- Sort preferences: sortBy, sortDirection
|
|
@@ -1043,12 +1043,11 @@ const handleSelect = (id) => {
|
|
|
1043
1043
|
selectedId: id
|
|
1044
1044
|
});
|
|
1045
1045
|
};`
|
|
1046
|
-
}
|
|
1046
|
+
};
|
|
1047
1047
|
break;
|
|
1048
1048
|
case 'pass-standard-props':
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
suggestion: 'Always pass standard props to all components',
|
|
1049
|
+
violation.suggestion = {
|
|
1050
|
+
text: 'Always pass standard props to all components',
|
|
1052
1051
|
example: `// Always include these props when calling components:
|
|
1053
1052
|
<ChildComponent
|
|
1054
1053
|
items={items} // Data props
|
|
@@ -1063,19 +1062,17 @@ const handleSelect = (id) => {
|
|
|
1063
1062
|
components={components}
|
|
1064
1063
|
callbacks={callbacks}
|
|
1065
1064
|
/>`
|
|
1066
|
-
}
|
|
1065
|
+
};
|
|
1067
1066
|
break;
|
|
1068
1067
|
case 'no-child-implementation':
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
suggestion: 'Remove child component implementations. Only the root component function should be in this file',
|
|
1068
|
+
violation.suggestion = {
|
|
1069
|
+
text: 'Remove child component implementations. Only the root component function should be in this file',
|
|
1072
1070
|
example: 'Move child component functions to separate generation requests'
|
|
1073
|
-
}
|
|
1071
|
+
};
|
|
1074
1072
|
break;
|
|
1075
1073
|
case 'undefined-component-usage':
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
suggestion: 'Ensure all components destructured from the components prop are defined in the component spec dependencies',
|
|
1074
|
+
violation.suggestion = {
|
|
1075
|
+
text: 'Ensure all components destructured from the components prop are defined in the component spec dependencies',
|
|
1079
1076
|
example: `// Component spec should include all referenced components:
|
|
1080
1077
|
{
|
|
1081
1078
|
"name": "MyComponent",
|
|
@@ -1100,12 +1097,11 @@ const handleSelect = (id) => {
|
|
|
1100
1097
|
// Then in your component:
|
|
1101
1098
|
const { ModelTreeView, PromptTable, FilterPanel } = components;
|
|
1102
1099
|
// All these will be available`
|
|
1103
|
-
}
|
|
1100
|
+
};
|
|
1104
1101
|
break;
|
|
1105
1102
|
case 'component-usage-without-destructuring':
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
suggestion: 'Components must be properly accessed - either destructure from components prop or use dot notation',
|
|
1103
|
+
violation.suggestion = {
|
|
1104
|
+
text: 'Components must be properly accessed - either destructure from components prop or use dot notation',
|
|
1109
1105
|
example: `// ❌ WRONG - Using component without destructuring:
|
|
1110
1106
|
function MyComponent({ components }) {
|
|
1111
1107
|
return <AccountList />; // Error: AccountList not destructured
|
|
@@ -1126,12 +1122,11 @@ function MyComponent({ components }) {
|
|
|
1126
1122
|
function MyComponent({ components: { AccountList } }) {
|
|
1127
1123
|
return <AccountList />;
|
|
1128
1124
|
}`
|
|
1129
|
-
}
|
|
1125
|
+
};
|
|
1130
1126
|
break;
|
|
1131
1127
|
case 'unsafe-array-access':
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
suggestion: 'Always check array bounds before accessing elements',
|
|
1128
|
+
violation.suggestion = {
|
|
1129
|
+
text: 'Always check array bounds before accessing elements',
|
|
1135
1130
|
example: `// ❌ UNSAFE:
|
|
1136
1131
|
const firstItem = items[0].name;
|
|
1137
1132
|
const total = data[0].reduce((sum, item) => sum + item.value, 0);
|
|
@@ -1145,12 +1140,11 @@ const total = data.length > 0
|
|
|
1145
1140
|
// ✅ BETTER - Use optional chaining:
|
|
1146
1141
|
const firstItem = items[0]?.name || 'No items';
|
|
1147
1142
|
const total = data[0]?.reduce((sum, item) => sum + item.value, 0) || 0;`
|
|
1148
|
-
}
|
|
1143
|
+
};
|
|
1149
1144
|
break;
|
|
1150
1145
|
case 'array-reduce-safety':
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
suggestion: 'Always provide an initial value for reduce() or check array length',
|
|
1146
|
+
violation.suggestion = {
|
|
1147
|
+
text: 'Always provide an initial value for reduce() or check array length',
|
|
1154
1148
|
example: `// ❌ UNSAFE:
|
|
1155
1149
|
const sum = numbers.reduce((a, b) => a + b); // Fails on empty array
|
|
1156
1150
|
const total = data[0].reduce((sum, item) => sum + item.value); // Multiple issues
|
|
@@ -1165,12 +1159,11 @@ const total = data.length > 0 && data[0]
|
|
|
1165
1159
|
const sum = numbers.length > 0
|
|
1166
1160
|
? numbers.reduce((a, b) => a + b)
|
|
1167
1161
|
: 0;`
|
|
1168
|
-
}
|
|
1162
|
+
};
|
|
1169
1163
|
break;
|
|
1170
1164
|
case 'entity-name-mismatch':
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
suggestion: 'Use the exact entity name from dataRequirements in RunView calls',
|
|
1165
|
+
violation.suggestion = {
|
|
1166
|
+
text: 'Use the exact entity name from dataRequirements in RunView calls',
|
|
1174
1167
|
example: `// The component spec defines the entities to use:
|
|
1175
1168
|
// dataRequirements: {
|
|
1176
1169
|
// entities: [
|
|
@@ -1198,12 +1191,11 @@ await utilities.rv.RunViews([
|
|
|
1198
1191
|
|
|
1199
1192
|
// The linter validates that all entity names in RunView/RunViews calls
|
|
1200
1193
|
// match those declared in the component spec's dataRequirements`
|
|
1201
|
-
}
|
|
1194
|
+
};
|
|
1202
1195
|
break;
|
|
1203
1196
|
case 'missing-query-parameter':
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
suggestion: 'Provide all required parameters defined in dataRequirements for the query',
|
|
1197
|
+
violation.suggestion = {
|
|
1198
|
+
text: 'Provide all required parameters defined in dataRequirements for the query',
|
|
1207
1199
|
example: `// The component spec defines required parameters:
|
|
1208
1200
|
// dataRequirements: {
|
|
1209
1201
|
// queries: [
|
|
@@ -1234,12 +1226,11 @@ await utilities.rq.RunQuery({
|
|
|
1234
1226
|
StartDate: startDate // All parameters included
|
|
1235
1227
|
}
|
|
1236
1228
|
});`
|
|
1237
|
-
}
|
|
1229
|
+
};
|
|
1238
1230
|
break;
|
|
1239
1231
|
case 'unknown-query-parameter':
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
suggestion: 'Only use parameters that are defined in dataRequirements for the query',
|
|
1232
|
+
violation.suggestion = {
|
|
1233
|
+
text: 'Only use parameters that are defined in dataRequirements for the query',
|
|
1243
1234
|
example: `// ❌ WRONG - Using undefined parameter:
|
|
1244
1235
|
await utilities.rq.RunQuery({
|
|
1245
1236
|
QueryName: "User Activity Summary",
|
|
@@ -1258,12 +1249,11 @@ await utilities.rq.RunQuery({
|
|
|
1258
1249
|
StartDate: startDate // Only parameters from dataRequirements
|
|
1259
1250
|
}
|
|
1260
1251
|
});`
|
|
1261
|
-
}
|
|
1252
|
+
};
|
|
1262
1253
|
break;
|
|
1263
1254
|
case 'missing-parameters-object':
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
suggestion: 'Queries with parameters must include a Parameters object in RunQuery',
|
|
1255
|
+
violation.suggestion = {
|
|
1256
|
+
text: 'Queries with parameters must include a Parameters object in RunQuery',
|
|
1267
1257
|
example: `// ❌ WRONG - Query requires parameters but none provided:
|
|
1268
1258
|
await utilities.rq.RunQuery({
|
|
1269
1259
|
QueryName: "User Activity Summary"
|
|
@@ -1278,12 +1268,11 @@ await utilities.rq.RunQuery({
|
|
|
1278
1268
|
StartDate: startDate
|
|
1279
1269
|
}
|
|
1280
1270
|
});`
|
|
1281
|
-
}
|
|
1271
|
+
};
|
|
1282
1272
|
break;
|
|
1283
1273
|
case 'query-name-mismatch':
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
suggestion: 'Use the exact query name from dataRequirements in RunQuery calls',
|
|
1274
|
+
violation.suggestion = {
|
|
1275
|
+
text: 'Use the exact query name from dataRequirements in RunQuery calls',
|
|
1287
1276
|
example: `// The component spec defines the queries to use:
|
|
1288
1277
|
// dataRequirements: {
|
|
1289
1278
|
// queries: [
|
|
@@ -1305,12 +1294,11 @@ await utilities.rv.RunQuery({
|
|
|
1305
1294
|
|
|
1306
1295
|
// The linter validates that all query names in RunQuery calls
|
|
1307
1296
|
// match those declared in the component spec's dataRequirements.queries`
|
|
1308
|
-
}
|
|
1297
|
+
};
|
|
1309
1298
|
break;
|
|
1310
1299
|
case 'runview-sql-function':
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
suggestion: 'RunView does not support SQL aggregations. Use RunQuery or aggregate in JavaScript.',
|
|
1300
|
+
violation.suggestion = {
|
|
1301
|
+
text: 'RunView does not support SQL aggregations. Use RunQuery or aggregate in JavaScript.',
|
|
1314
1302
|
example: `// ❌ WRONG - SQL functions in RunView:
|
|
1315
1303
|
await utilities.rv.RunView({
|
|
1316
1304
|
EntityName: 'Accounts',
|
|
@@ -1332,12 +1320,11 @@ if (result?.Success) {
|
|
|
1332
1320
|
const total = result.Results.length;
|
|
1333
1321
|
const totalRevenue = result.Results.reduce((sum, acc) => sum + (acc.Revenue || 0), 0);
|
|
1334
1322
|
}`
|
|
1335
|
-
}
|
|
1323
|
+
};
|
|
1336
1324
|
break;
|
|
1337
1325
|
case 'field-not-in-requirements':
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
suggestion: 'Only use fields that are defined in dataRequirements for the entity',
|
|
1326
|
+
violation.suggestion = {
|
|
1327
|
+
text: 'Only use fields that are defined in dataRequirements for the entity',
|
|
1341
1328
|
example: `// Check your dataRequirements to see allowed fields:
|
|
1342
1329
|
// dataRequirements: {
|
|
1343
1330
|
// entities: [{
|
|
@@ -1359,12 +1346,11 @@ await utilities.rv.RunView({
|
|
|
1359
1346
|
EntityName: 'Accounts',
|
|
1360
1347
|
Fields: ['ID', 'AccountName', 'Industry'] // All from displayFields
|
|
1361
1348
|
});`
|
|
1362
|
-
}
|
|
1349
|
+
};
|
|
1363
1350
|
break;
|
|
1364
1351
|
case 'orderby-field-not-sortable':
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
suggestion: 'OrderBy fields must be in the sortFields array for the entity',
|
|
1352
|
+
violation.suggestion = {
|
|
1353
|
+
text: 'OrderBy fields must be in the sortFields array for the entity',
|
|
1368
1354
|
example: `// ❌ WRONG - Sorting by non-sortable field:
|
|
1369
1355
|
await utilities.rv.RunView({
|
|
1370
1356
|
EntityName: 'Accounts',
|
|
@@ -1376,12 +1362,11 @@ await utilities.rv.RunView({
|
|
|
1376
1362
|
EntityName: 'Accounts',
|
|
1377
1363
|
OrderBy: 'AccountName ASC' // AccountName is in sortFields
|
|
1378
1364
|
});`
|
|
1379
|
-
}
|
|
1365
|
+
};
|
|
1380
1366
|
break;
|
|
1381
1367
|
case 'parent-event-callback-usage':
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
suggestion: 'Components must invoke parent event callbacks when state changes',
|
|
1368
|
+
violation.suggestion = {
|
|
1369
|
+
text: 'Components must invoke parent event callbacks when state changes',
|
|
1385
1370
|
example: `// ❌ WRONG - Only updating internal state:
|
|
1386
1371
|
function ChildComponent({ onSelectAccount, savedUserSettings, onSaveUserSettings }) {
|
|
1387
1372
|
const [selectedAccountId, setSelectedAccountId] = useState(savedUserSettings?.selectedAccountId);
|
|
@@ -1410,12 +1395,11 @@ function ChildComponent({ onSelectAccount, savedUserSettings, onSaveUserSettings
|
|
|
1410
1395
|
onSaveUserSettings?.({ ...savedUserSettings, selectedAccountId: accountId });
|
|
1411
1396
|
};
|
|
1412
1397
|
}`
|
|
1413
|
-
}
|
|
1398
|
+
};
|
|
1414
1399
|
break;
|
|
1415
1400
|
case 'property-name-consistency':
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
suggestion: 'Maintain consistent property names when transforming data',
|
|
1401
|
+
violation.suggestion = {
|
|
1402
|
+
text: 'Maintain consistent property names when transforming data',
|
|
1419
1403
|
example: `// ❌ WRONG - Transform to camelCase but access as PascalCase:
|
|
1420
1404
|
setAccountData(results.map(item => ({
|
|
1421
1405
|
accountName: item.AccountName, // camelCase
|
|
@@ -1442,12 +1426,11 @@ setAccountData(results.map(item => ({
|
|
|
1442
1426
|
// Later in render...
|
|
1443
1427
|
<td>{account.accountName}</td> // Use camelCase consistently
|
|
1444
1428
|
<td>{formatCurrency(account.annualRevenue)}</td> // Works!`
|
|
1445
|
-
}
|
|
1429
|
+
};
|
|
1446
1430
|
break;
|
|
1447
1431
|
case 'noisy-settings-updates':
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
suggestion: 'Save settings sparingly - only on meaningful user actions',
|
|
1432
|
+
violation.suggestion = {
|
|
1433
|
+
text: 'Save settings sparingly - only on meaningful user actions',
|
|
1451
1434
|
example: `// ❌ WRONG - Saving on every keystroke:
|
|
1452
1435
|
const handleSearchChange = (e) => {
|
|
1453
1436
|
setSearchTerm(e.target.value);
|
|
@@ -1468,12 +1451,11 @@ const saveSearchTerm = useMemo(() =>
|
|
|
1468
1451
|
}, 500),
|
|
1469
1452
|
[savedUserSettings]
|
|
1470
1453
|
);`
|
|
1471
|
-
}
|
|
1454
|
+
};
|
|
1472
1455
|
break;
|
|
1473
1456
|
case 'prop-state-sync':
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
suggestion: 'Initialize state once, don\'t sync from props',
|
|
1457
|
+
violation.suggestion = {
|
|
1458
|
+
text: 'Initialize state once, don\'t sync from props',
|
|
1477
1459
|
example: `// ❌ WRONG - Syncing prop to state:
|
|
1478
1460
|
const [value, setValue] = useState(propValue);
|
|
1479
1461
|
useEffect(() => {
|
|
@@ -1487,12 +1469,11 @@ const [value, setValue] = useState(
|
|
|
1487
1469
|
|
|
1488
1470
|
// ✅ CORRECT - If you need prop changes, use derived state:
|
|
1489
1471
|
const displayValue = propOverride || value;`
|
|
1490
|
-
}
|
|
1472
|
+
};
|
|
1491
1473
|
break;
|
|
1492
1474
|
case 'performance-memoization':
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
suggestion: 'Use useMemo for expensive operations and static data',
|
|
1475
|
+
violation.suggestion = {
|
|
1476
|
+
text: 'Use useMemo for expensive operations and static data',
|
|
1496
1477
|
example: `// ❌ WRONG - Expensive operation on every render:
|
|
1497
1478
|
const filteredItems = items.filter(item =>
|
|
1498
1479
|
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
@@ -1517,12 +1498,11 @@ const columns = useMemo(() => [
|
|
|
1517
1498
|
{ field: 'name', header: 'Name' },
|
|
1518
1499
|
{ field: 'value', header: 'Value' }
|
|
1519
1500
|
], []); // Empty deps = never changes`
|
|
1520
|
-
}
|
|
1501
|
+
};
|
|
1521
1502
|
break;
|
|
1522
1503
|
case 'child-state-management':
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
suggestion: 'Never manage state for child components',
|
|
1504
|
+
violation.suggestion = {
|
|
1505
|
+
text: 'Never manage state for child components',
|
|
1526
1506
|
example: `// ❌ WRONG - Managing child state:
|
|
1527
1507
|
const [childTableSort, setChildTableSort] = useState('name');
|
|
1528
1508
|
const [modalOpen, setModalOpen] = useState(false);
|
|
@@ -1539,12 +1519,11 @@ const [modalOpen, setModalOpen] = useState(false);
|
|
|
1539
1519
|
onSaveUserSettings={handleChildSettings}
|
|
1540
1520
|
// Child manages its own sort state!
|
|
1541
1521
|
/>`
|
|
1542
|
-
}
|
|
1522
|
+
};
|
|
1543
1523
|
break;
|
|
1544
1524
|
case 'server-reload-on-client-operation':
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
suggestion: 'Use client-side operations for sorting and filtering',
|
|
1525
|
+
violation.suggestion = {
|
|
1526
|
+
text: 'Use client-side operations for sorting and filtering',
|
|
1548
1527
|
example: `// ❌ WRONG - Reload from server:
|
|
1549
1528
|
const handleSort = (field) => {
|
|
1550
1529
|
setSortBy(field);
|
|
@@ -1568,12 +1547,11 @@ const sortedData = useMemo(() => {
|
|
|
1568
1547
|
});
|
|
1569
1548
|
return sorted;
|
|
1570
1549
|
}, [data, sortBy, sortDirection]);`
|
|
1571
|
-
}
|
|
1550
|
+
};
|
|
1572
1551
|
break;
|
|
1573
1552
|
case 'runview-runquery-valid-properties':
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
suggestion: 'Use only valid properties for RunView/RunViews and RunQuery',
|
|
1553
|
+
violation.suggestion = {
|
|
1554
|
+
text: 'Use only valid properties for RunView/RunViews and RunQuery',
|
|
1577
1555
|
example: `// ❌ WRONG - Invalid properties on RunView:
|
|
1578
1556
|
await utilities.rv.RunView({
|
|
1579
1557
|
EntityName: 'MJ: AI Prompt Runs',
|
|
@@ -1602,23 +1580,34 @@ await utilities.rq.RunQuery({
|
|
|
1602
1580
|
// Valid RunQuery properties:
|
|
1603
1581
|
// - QueryName (required)
|
|
1604
1582
|
// - CategoryName, CategoryID, Parameters (optional)`
|
|
1605
|
-
}
|
|
1583
|
+
};
|
|
1606
1584
|
break;
|
|
1607
1585
|
case 'component-props-validation':
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
// Additional props will break hosting environment
|
|
1586
|
+
violation.suggestion = {
|
|
1587
|
+
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.',
|
|
1588
|
+
example: `// ❌ WRONG - Component with undeclared props:
|
|
1589
|
+
function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
|
|
1590
|
+
// customers, orders, selectedId are NOT allowed unless defined in spec
|
|
1614
1591
|
}
|
|
1615
1592
|
|
|
1616
|
-
// ✅ CORRECT
|
|
1617
|
-
function
|
|
1618
|
-
// Load
|
|
1593
|
+
// ✅ CORRECT Option 1 - Use only standard props and load data internally:
|
|
1594
|
+
function MyComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
|
|
1595
|
+
// Load data internally using utilities
|
|
1619
1596
|
const [customers, setCustomers] = useState([]);
|
|
1620
1597
|
const [orders, setOrders] = useState([]);
|
|
1621
1598
|
const [selectedId, setSelectedId] = useState(savedUserSettings?.selectedId);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// ✅ CORRECT Option 2 - Define props in component spec:
|
|
1602
|
+
// In spec.properties array:
|
|
1603
|
+
// [
|
|
1604
|
+
// { name: "customers", type: "array", required: false, description: "Customer list" },
|
|
1605
|
+
// { name: "orders", type: "array", required: false, description: "Order list" },
|
|
1606
|
+
// { name: "selectedId", type: "string", required: false, description: "Selected item ID" }
|
|
1607
|
+
// ]
|
|
1608
|
+
// Then the component can accept them:
|
|
1609
|
+
function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
|
|
1610
|
+
// These props are now allowed because they're defined in the spec
|
|
1622
1611
|
|
|
1623
1612
|
useEffect(() => {
|
|
1624
1613
|
const loadData = async () => {
|
|
@@ -1639,12 +1628,11 @@ function RootComponent({ utilities, styles, components, callbacks, savedUserSett
|
|
|
1639
1628
|
|
|
1640
1629
|
return <div>{/* Use state, not props */}</div>;
|
|
1641
1630
|
}`
|
|
1642
|
-
}
|
|
1631
|
+
};
|
|
1643
1632
|
break;
|
|
1644
1633
|
case 'runview-runquery-result-direct-usage':
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
suggestion: 'RunView and RunQuery return result objects, not arrays. Access the data with .Results property.',
|
|
1634
|
+
violation.suggestion = {
|
|
1635
|
+
text: 'RunView and RunQuery return result objects, not arrays. Access the data with .Results property.',
|
|
1648
1636
|
example: `// ❌ WRONG - Using result directly as array:
|
|
1649
1637
|
const result = await utilities.rv.RunView({
|
|
1650
1638
|
EntityName: 'Users',
|
|
@@ -1690,12 +1678,11 @@ setData(queryResult.Results || []); // NOT queryResult directly!
|
|
|
1690
1678
|
// TotalRowCount?: number,
|
|
1691
1679
|
// ExecutionTime?: number
|
|
1692
1680
|
// }`
|
|
1693
|
-
}
|
|
1681
|
+
};
|
|
1694
1682
|
break;
|
|
1695
1683
|
case 'styles-invalid-path':
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
suggestion: 'Fix invalid styles property paths. Use the correct ComponentStyles interface structure.',
|
|
1684
|
+
violation.suggestion = {
|
|
1685
|
+
text: 'Fix invalid styles property paths. Use the correct ComponentStyles interface structure.',
|
|
1699
1686
|
example: `// ❌ WRONG - Invalid property paths:
|
|
1700
1687
|
styles.fontSize.small // fontSize is not at root level
|
|
1701
1688
|
styles.colors.background // colors.background exists
|
|
@@ -1710,12 +1697,11 @@ styles.spacing.sm // correct size name
|
|
|
1710
1697
|
styles?.typography?.fontSize?.sm || '14px'
|
|
1711
1698
|
styles?.colors?.background || '#FFFFFF'
|
|
1712
1699
|
styles?.spacing?.sm || '8px'`
|
|
1713
|
-
}
|
|
1700
|
+
};
|
|
1714
1701
|
break;
|
|
1715
1702
|
case 'styles-unsafe-access':
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
suggestion: 'Use optional chaining for nested styles access to prevent runtime errors.',
|
|
1703
|
+
violation.suggestion = {
|
|
1704
|
+
text: 'Use optional chaining for nested styles access to prevent runtime errors.',
|
|
1719
1705
|
example: `// ❌ UNSAFE - Direct nested access:
|
|
1720
1706
|
const fontSize = styles.typography.fontSize.md;
|
|
1721
1707
|
const borderRadius = styles.borders.radius.sm;
|
|
@@ -1730,45 +1716,39 @@ const {
|
|
|
1730
1716
|
fontSize: { md: fontSize = '14px' } = {}
|
|
1731
1717
|
} = {}
|
|
1732
1718
|
} = styles || {};`
|
|
1733
|
-
}
|
|
1719
|
+
};
|
|
1734
1720
|
break;
|
|
1735
1721
|
}
|
|
1736
1722
|
}
|
|
1737
|
-
return
|
|
1723
|
+
return violations;
|
|
1738
1724
|
}
|
|
1739
1725
|
static generateSyntaxErrorSuggestions(violations) {
|
|
1740
|
-
const suggestions = [];
|
|
1741
1726
|
for (const violation of violations) {
|
|
1742
1727
|
if (violation.message.includes('Unterminated string')) {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
suggestion: 'Check that all string literals are properly closed with matching quotes',
|
|
1728
|
+
violation.suggestion = {
|
|
1729
|
+
text: 'Check that all string literals are properly closed with matching quotes',
|
|
1746
1730
|
example: 'Template literals with interpolation must use backticks: `text ${variable} text`'
|
|
1747
|
-
}
|
|
1731
|
+
};
|
|
1748
1732
|
}
|
|
1749
1733
|
else if (violation.message.includes('Unexpected token') || violation.message.includes('export')) {
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
suggestion: 'Ensure all code is within the component function body',
|
|
1734
|
+
violation.suggestion = {
|
|
1735
|
+
text: 'Ensure all code is within the component function body',
|
|
1753
1736
|
example: 'Remove any export statements or code outside the function definition'
|
|
1754
|
-
}
|
|
1737
|
+
};
|
|
1755
1738
|
}
|
|
1756
1739
|
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',
|
|
1740
|
+
violation.suggestion = {
|
|
1741
|
+
text: 'Import statements are not allowed in components - use props instead',
|
|
1760
1742
|
example: 'Access libraries through props: const { React, MaterialUI } = props.components'
|
|
1761
|
-
}
|
|
1743
|
+
};
|
|
1762
1744
|
}
|
|
1763
1745
|
else {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
suggestion: 'Fix the syntax error before the component can be compiled',
|
|
1746
|
+
violation.suggestion = {
|
|
1747
|
+
text: 'Fix the syntax error before the component can be compiled',
|
|
1767
1748
|
example: 'Review the code at the specified line and column for syntax issues'
|
|
1768
|
-
}
|
|
1749
|
+
};
|
|
1769
1750
|
}
|
|
1770
1751
|
}
|
|
1771
|
-
return suggestions;
|
|
1772
1752
|
}
|
|
1773
1753
|
/**
|
|
1774
1754
|
* Apply library-specific lint rules based on ComponentLibrary LintRules field
|
|
@@ -1786,50 +1766,78 @@ const {
|
|
|
1786
1766
|
const libraryViolations = [];
|
|
1787
1767
|
// Get the cached and compiled rules for this library
|
|
1788
1768
|
const compiledRules = cache.getLibraryRules(lib.name);
|
|
1769
|
+
if (debugMode) {
|
|
1770
|
+
console.log(`\n 📚 Library: ${lib.name}`);
|
|
1771
|
+
if (compiledRules) {
|
|
1772
|
+
console.log(` ┌─ Has lint rules: ✅`);
|
|
1773
|
+
if (compiledRules.validators) {
|
|
1774
|
+
console.log(` ├─ Validators: ${Object.keys(compiledRules.validators).length}`);
|
|
1775
|
+
}
|
|
1776
|
+
if (compiledRules.initialization) {
|
|
1777
|
+
console.log(` ├─ Initialization rules: ✅`);
|
|
1778
|
+
}
|
|
1779
|
+
if (compiledRules.lifecycle) {
|
|
1780
|
+
console.log(` ├─ Lifecycle rules: ✅`);
|
|
1781
|
+
}
|
|
1782
|
+
console.log(` └─ Starting checks...`);
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
console.log(` └─ No lint rules defined`);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1789
1788
|
if (compiledRules) {
|
|
1790
1789
|
const library = compiledRules.library;
|
|
1791
1790
|
const libraryName = library.Name || lib.name;
|
|
1792
1791
|
// Apply initialization rules
|
|
1793
1792
|
if (compiledRules.initialization) {
|
|
1793
|
+
if (debugMode) {
|
|
1794
|
+
console.log(` ├─ 🔍 Checking ${libraryName} initialization patterns...`);
|
|
1795
|
+
}
|
|
1794
1796
|
const initViolations = this.checkLibraryInitialization(ast, libraryName, compiledRules.initialization);
|
|
1795
1797
|
// Debug logging for library violations
|
|
1796
1798
|
if (debugMode && initViolations.length > 0) {
|
|
1797
|
-
console.log(
|
|
1799
|
+
console.log(` │ ⚠️ Found ${initViolations.length} initialization issue${initViolations.length > 1 ? 's' : ''}`);
|
|
1798
1800
|
initViolations.forEach(v => {
|
|
1799
1801
|
const icon = v.severity === 'critical' ? '🔴' :
|
|
1800
1802
|
v.severity === 'high' ? '🟠' :
|
|
1801
1803
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
1802
|
-
console.log(` ${icon}
|
|
1804
|
+
console.log(` │ ${icon} Line ${v.line}: ${v.message}`);
|
|
1803
1805
|
});
|
|
1804
1806
|
}
|
|
1805
1807
|
libraryViolations.push(...initViolations);
|
|
1806
1808
|
}
|
|
1807
1809
|
// Apply lifecycle rules
|
|
1808
1810
|
if (compiledRules.lifecycle) {
|
|
1811
|
+
if (debugMode) {
|
|
1812
|
+
console.log(` ├─ 🔄 Checking ${libraryName} lifecycle management...`);
|
|
1813
|
+
}
|
|
1809
1814
|
const lifecycleViolations = this.checkLibraryLifecycle(ast, libraryName, compiledRules.lifecycle);
|
|
1810
1815
|
// Debug logging for library violations
|
|
1811
1816
|
if (debugMode && lifecycleViolations.length > 0) {
|
|
1812
|
-
console.log(
|
|
1817
|
+
console.log(` │ ⚠️ Found ${lifecycleViolations.length} lifecycle issue${lifecycleViolations.length > 1 ? 's' : ''}`);
|
|
1813
1818
|
lifecycleViolations.forEach(v => {
|
|
1814
1819
|
const icon = v.severity === 'critical' ? '🔴' :
|
|
1815
1820
|
v.severity === 'high' ? '🟠' :
|
|
1816
1821
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
1817
|
-
console.log(` ${icon}
|
|
1822
|
+
console.log(` │ ${icon} Line ${v.line}: ${v.message}`);
|
|
1818
1823
|
});
|
|
1819
1824
|
}
|
|
1820
1825
|
libraryViolations.push(...lifecycleViolations);
|
|
1821
1826
|
}
|
|
1822
1827
|
// Apply options validation
|
|
1823
1828
|
if (compiledRules.options) {
|
|
1829
|
+
if (debugMode) {
|
|
1830
|
+
console.log(` ├─ ⚙️ Checking ${libraryName} configuration options...`);
|
|
1831
|
+
}
|
|
1824
1832
|
const optionsViolations = this.checkLibraryOptions(ast, libraryName, compiledRules.options);
|
|
1825
1833
|
// Debug logging for library violations
|
|
1826
1834
|
if (debugMode && optionsViolations.length > 0) {
|
|
1827
|
-
console.log(
|
|
1835
|
+
console.log(` │ ⚠️ Found ${optionsViolations.length} configuration issue${optionsViolations.length > 1 ? 's' : ''}`);
|
|
1828
1836
|
optionsViolations.forEach(v => {
|
|
1829
1837
|
const icon = v.severity === 'critical' ? '🔴' :
|
|
1830
1838
|
v.severity === 'high' ? '🟠' :
|
|
1831
1839
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
1832
|
-
console.log(` ${icon}
|
|
1840
|
+
console.log(` │ ${icon} Line ${v.line}: ${v.message}`);
|
|
1833
1841
|
});
|
|
1834
1842
|
}
|
|
1835
1843
|
libraryViolations.push(...optionsViolations);
|
|
@@ -2129,6 +2137,13 @@ const {
|
|
|
2129
2137
|
for (const [validatorName, validator] of Object.entries(validators)) {
|
|
2130
2138
|
if (validator && validator.validateFn) {
|
|
2131
2139
|
const beforeCount = context.violations.length;
|
|
2140
|
+
// Log that we're running this specific validator
|
|
2141
|
+
if (debugMode) {
|
|
2142
|
+
console.log(` ├─ 🔬 Running ${libraryName} validator: ${validatorName}`);
|
|
2143
|
+
if (validator.description) {
|
|
2144
|
+
console.log(` │ ℹ️ ${validator.description}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2132
2147
|
// Traverse AST and apply validator
|
|
2133
2148
|
(0, traverse_1.default)(ast, {
|
|
2134
2149
|
enter(path) {
|
|
@@ -2139,27 +2154,31 @@ const {
|
|
|
2139
2154
|
catch (error) {
|
|
2140
2155
|
// Validator execution error - log but don't crash
|
|
2141
2156
|
console.warn(`Validator ${validatorName} failed:`, error);
|
|
2157
|
+
if (debugMode) {
|
|
2158
|
+
console.error('Full error:', error);
|
|
2159
|
+
}
|
|
2142
2160
|
}
|
|
2143
2161
|
}
|
|
2144
2162
|
});
|
|
2145
2163
|
// Debug logging for this specific validator
|
|
2146
2164
|
const newViolations = context.violations.length - beforeCount;
|
|
2147
2165
|
if (debugMode && newViolations > 0) {
|
|
2148
|
-
console.log(
|
|
2149
|
-
console.log(` 📊 ${validator.description || 'No description'}`);
|
|
2150
|
-
console.log(` ⚠️ Found ${newViolations} violation${newViolations > 1 ? 's' : ''}`);
|
|
2166
|
+
console.log(` │ ✓ Found ${newViolations} violation${newViolations > 1 ? 's' : ''}`);
|
|
2151
2167
|
// Show the violations from this validator
|
|
2152
2168
|
const validatorViolations = context.violations.slice(beforeCount);
|
|
2153
2169
|
validatorViolations.forEach((v) => {
|
|
2154
2170
|
const icon = v.type === 'error' || v.severity === 'critical' ? '🔴' :
|
|
2155
2171
|
v.type === 'warning' || v.severity === 'high' ? '🟠' :
|
|
2156
2172
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
2157
|
-
console.log(`
|
|
2173
|
+
console.log(` │ ${icon} Line ${v.line || 'unknown'}: ${v.message}`);
|
|
2158
2174
|
if (v.suggestion) {
|
|
2159
|
-
console.log(`
|
|
2175
|
+
console.log(` │ 💡 ${v.suggestion}`);
|
|
2160
2176
|
}
|
|
2161
2177
|
});
|
|
2162
2178
|
}
|
|
2179
|
+
else if (debugMode) {
|
|
2180
|
+
console.log(` │ ✓ No violations found`);
|
|
2181
|
+
}
|
|
2163
2182
|
}
|
|
2164
2183
|
}
|
|
2165
2184
|
// Convert context violations to standard format
|
|
@@ -2513,7 +2532,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2513
2532
|
severity: 'critical',
|
|
2514
2533
|
line: path.node.loc?.start.line || 0,
|
|
2515
2534
|
column: path.node.loc?.start.column || 0,
|
|
2516
|
-
message: `Component '${varName}' shadows a dependency component. The component '${varName}'
|
|
2535
|
+
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
2536
|
code: `const ${varName} = ...`
|
|
2518
2537
|
});
|
|
2519
2538
|
}
|
|
@@ -2531,15 +2550,15 @@ ComponentLinter.universalComponentRules = [
|
|
|
2531
2550
|
severity: 'critical',
|
|
2532
2551
|
line: path.node.loc?.start.line || 0,
|
|
2533
2552
|
column: path.node.loc?.start.column || 0,
|
|
2534
|
-
message: `Component '${funcName}' shadows a dependency component. The component '${funcName}'
|
|
2553
|
+
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
2554
|
code: `function ${funcName}(...)`
|
|
2536
2555
|
});
|
|
2537
2556
|
}
|
|
2538
2557
|
}
|
|
2539
2558
|
}
|
|
2540
2559
|
});
|
|
2541
|
-
// Components
|
|
2542
|
-
//
|
|
2560
|
+
// Components must be destructured from the components prop or accessed via components.ComponentName
|
|
2561
|
+
// Check if they're being used correctly
|
|
2543
2562
|
let hasComponentsUsage = false;
|
|
2544
2563
|
const usedDependencies = new Set();
|
|
2545
2564
|
mainComponentPath.traverse({
|
|
@@ -2580,7 +2599,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2580
2599
|
}
|
|
2581
2600
|
}
|
|
2582
2601
|
});
|
|
2583
|
-
//
|
|
2602
|
+
// Check for unused dependencies - components must be destructured or accessed via components prop
|
|
2584
2603
|
if (dependencyNames.size > 0 && usedDependencies.size === 0) {
|
|
2585
2604
|
const depList = Array.from(dependencyNames).join(', ');
|
|
2586
2605
|
violations.push({
|
|
@@ -2588,7 +2607,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2588
2607
|
severity: 'low',
|
|
2589
2608
|
line: mainComponentPath.node.loc?.start.line || 0,
|
|
2590
2609
|
column: mainComponentPath.node.loc?.start.column || 0,
|
|
2591
|
-
message: `Component has dependencies [${depList}] defined in spec but they're not being used. These components
|
|
2610
|
+
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
2611
|
code: `// Available: ${depList}`
|
|
2593
2612
|
});
|
|
2594
2613
|
}
|
|
@@ -4954,10 +4973,13 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4954
4973
|
// Build set of allowed props: standard props + React special props + componentSpec properties
|
|
4955
4974
|
const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
|
|
4956
4975
|
// Add props from componentSpec.properties if they exist
|
|
4976
|
+
// These are the architect-defined props that this component is allowed to accept
|
|
4977
|
+
const specDefinedProps = [];
|
|
4957
4978
|
if (componentSpec?.properties) {
|
|
4958
4979
|
for (const prop of componentSpec.properties) {
|
|
4959
4980
|
if (prop.name) {
|
|
4960
4981
|
allowedProps.add(prop.name);
|
|
4982
|
+
specDefinedProps.push(prop.name);
|
|
4961
4983
|
}
|
|
4962
4984
|
}
|
|
4963
4985
|
}
|
|
@@ -4979,15 +5001,28 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4979
5001
|
}
|
|
4980
5002
|
// Only report if there are non-allowed props
|
|
4981
5003
|
if (invalidProps.length > 0) {
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
: ''
|
|
5004
|
+
let message;
|
|
5005
|
+
if (specDefinedProps.length > 0) {
|
|
5006
|
+
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5007
|
+
`This component can only accept: ` +
|
|
5008
|
+
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5009
|
+
`(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
|
|
5010
|
+
`(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5011
|
+
`Any additional props must be defined in the component spec's properties array.`;
|
|
5012
|
+
}
|
|
5013
|
+
else {
|
|
5014
|
+
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5015
|
+
`This component can only accept: ` +
|
|
5016
|
+
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5017
|
+
`(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5018
|
+
`To accept additional props, they must be defined in the component spec's properties array.`;
|
|
5019
|
+
}
|
|
4985
5020
|
violations.push({
|
|
4986
5021
|
rule: 'component-props-validation',
|
|
4987
5022
|
severity: 'critical',
|
|
4988
5023
|
line: path.node.loc?.start.line || 0,
|
|
4989
5024
|
column: path.node.loc?.start.column || 0,
|
|
4990
|
-
message
|
|
5025
|
+
message
|
|
4991
5026
|
});
|
|
4992
5027
|
}
|
|
4993
5028
|
}
|
|
@@ -5012,15 +5047,28 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5012
5047
|
}
|
|
5013
5048
|
}
|
|
5014
5049
|
if (invalidProps.length > 0) {
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
: ''
|
|
5050
|
+
let message;
|
|
5051
|
+
if (specDefinedProps.length > 0) {
|
|
5052
|
+
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5053
|
+
`This component can only accept: ` +
|
|
5054
|
+
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5055
|
+
`(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
|
|
5056
|
+
`(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5057
|
+
`Any additional props must be defined in the component spec's properties array.`;
|
|
5058
|
+
}
|
|
5059
|
+
else {
|
|
5060
|
+
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5061
|
+
`This component can only accept: ` +
|
|
5062
|
+
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5063
|
+
`(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5064
|
+
`To accept additional props, they must be defined in the component spec's properties array.`;
|
|
5065
|
+
}
|
|
5018
5066
|
violations.push({
|
|
5019
5067
|
rule: 'component-props-validation',
|
|
5020
5068
|
severity: 'critical',
|
|
5021
5069
|
line: path.node.loc?.start.line || 0,
|
|
5022
5070
|
column: path.node.loc?.start.column || 0,
|
|
5023
|
-
message
|
|
5071
|
+
message
|
|
5024
5072
|
});
|
|
5025
5073
|
}
|
|
5026
5074
|
}
|
|
@@ -5253,87 +5301,6 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5253
5301
|
return violations;
|
|
5254
5302
|
}
|
|
5255
5303
|
},
|
|
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
5304
|
{
|
|
5338
5305
|
name: 'unsafe-array-operations',
|
|
5339
5306
|
appliesTo: 'all',
|
|
@@ -5727,14 +5694,14 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5727
5694
|
});
|
|
5728
5695
|
}
|
|
5729
5696
|
} else if (componentsFromProp.has(tagName)) {
|
|
5730
|
-
//
|
|
5731
|
-
//
|
|
5697
|
+
// Component is in dependencies but not destructured/accessible
|
|
5698
|
+
// This indicates the component wasn't properly destructured from components prop
|
|
5732
5699
|
violations.push({
|
|
5733
5700
|
rule: 'undefined-jsx-component',
|
|
5734
5701
|
severity: 'high',
|
|
5735
5702
|
line: openingElement.loc?.start.line || 0,
|
|
5736
5703
|
column: openingElement.loc?.start.column || 0,
|
|
5737
|
-
message: `JSX component "${tagName}" is in dependencies but appears to be undefined.
|
|
5704
|
+
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
5705
|
code: `<${tagName} ... />`
|
|
5739
5706
|
});
|
|
5740
5707
|
} else {
|
|
@@ -7543,87 +7510,117 @@ Correct pattern:
|
|
|
7543
7510
|
appliesTo: 'all',
|
|
7544
7511
|
test: (ast, componentName) => {
|
|
7545
7512
|
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);
|
|
7513
|
+
// Check that the AST body contains exactly one statement and it's a function declaration
|
|
7514
|
+
const programBody = ast.program.body;
|
|
7515
|
+
// First, check if there's anything other than a single function declaration
|
|
7516
|
+
if (programBody.length === 0) {
|
|
7517
|
+
violations.push({
|
|
7518
|
+
rule: 'single-function-only',
|
|
7519
|
+
severity: 'critical',
|
|
7520
|
+
line: 1,
|
|
7521
|
+
column: 0,
|
|
7522
|
+
message: `Component code must contain exactly one function declaration named "${componentName}". No code found.`,
|
|
7523
|
+
code: `Add: function ${componentName}({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) { ... }`
|
|
7524
|
+
});
|
|
7525
|
+
return violations;
|
|
7526
|
+
}
|
|
7527
|
+
if (programBody.length > 1) {
|
|
7528
|
+
// Multiple top-level statements - not allowed
|
|
7587
7529
|
violations.push({
|
|
7588
7530
|
rule: 'single-function-only',
|
|
7589
7531
|
severity: 'critical',
|
|
7590
|
-
line:
|
|
7591
|
-
column:
|
|
7592
|
-
message: `Component code must contain ONLY
|
|
7593
|
-
code: `Remove
|
|
7532
|
+
line: programBody[1].loc?.start.line || 0,
|
|
7533
|
+
column: programBody[1].loc?.start.column || 0,
|
|
7534
|
+
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.`,
|
|
7535
|
+
code: `Remove all code except: function ${componentName}(...) { ... }`
|
|
7594
7536
|
});
|
|
7595
|
-
//
|
|
7596
|
-
for (
|
|
7537
|
+
// Report each extra statement
|
|
7538
|
+
for (let i = 1; i < programBody.length; i++) {
|
|
7539
|
+
const stmt = programBody[i];
|
|
7540
|
+
let stmtType = 'statement';
|
|
7541
|
+
if (t.isVariableDeclaration(stmt)) {
|
|
7542
|
+
stmtType = 'variable declaration';
|
|
7543
|
+
}
|
|
7544
|
+
else if (t.isFunctionDeclaration(stmt)) {
|
|
7545
|
+
stmtType = 'function declaration';
|
|
7546
|
+
}
|
|
7547
|
+
else if (t.isExpressionStatement(stmt)) {
|
|
7548
|
+
stmtType = 'expression';
|
|
7549
|
+
}
|
|
7597
7550
|
violations.push({
|
|
7598
7551
|
rule: 'single-function-only',
|
|
7599
7552
|
severity: 'critical',
|
|
7600
|
-
line:
|
|
7601
|
-
column:
|
|
7602
|
-
message: `Extra
|
|
7603
|
-
code:
|
|
7553
|
+
line: stmt.loc?.start.line || 0,
|
|
7554
|
+
column: stmt.loc?.start.column || 0,
|
|
7555
|
+
message: `Extra ${stmtType} not allowed. Only the component function should exist.`,
|
|
7556
|
+
code: ''
|
|
7604
7557
|
});
|
|
7605
7558
|
}
|
|
7606
7559
|
}
|
|
7607
|
-
//
|
|
7608
|
-
|
|
7560
|
+
// Check that the single statement is a function declaration (not arrow function or other)
|
|
7561
|
+
const firstStatement = programBody[0];
|
|
7562
|
+
if (!t.isFunctionDeclaration(firstStatement)) {
|
|
7563
|
+
let actualType = 'unknown statement';
|
|
7564
|
+
let suggestion = '';
|
|
7565
|
+
if (t.isVariableDeclaration(firstStatement)) {
|
|
7566
|
+
// Check if it's an arrow function or other variable
|
|
7567
|
+
const declarator = firstStatement.declarations[0];
|
|
7568
|
+
if (t.isVariableDeclarator(declarator)) {
|
|
7569
|
+
if (t.isArrowFunctionExpression(declarator.init) || t.isFunctionExpression(declarator.init)) {
|
|
7570
|
+
actualType = 'arrow function or function expression';
|
|
7571
|
+
suggestion = `Use function declaration syntax: function ${componentName}(...) { ... }`;
|
|
7572
|
+
}
|
|
7573
|
+
else {
|
|
7574
|
+
actualType = 'variable declaration';
|
|
7575
|
+
suggestion = 'Remove this variable and ensure only the component function exists';
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
else if (t.isExpressionStatement(firstStatement)) {
|
|
7580
|
+
actualType = 'expression statement';
|
|
7581
|
+
suggestion = 'Remove this expression and add the component function';
|
|
7582
|
+
}
|
|
7609
7583
|
violations.push({
|
|
7610
7584
|
rule: 'single-function-only',
|
|
7611
7585
|
severity: 'critical',
|
|
7612
|
-
line:
|
|
7613
|
-
column:
|
|
7614
|
-
message: `Component
|
|
7615
|
-
code:
|
|
7586
|
+
line: firstStatement.loc?.start.line || 0,
|
|
7587
|
+
column: firstStatement.loc?.start.column || 0,
|
|
7588
|
+
message: `Component must be a function declaration, not ${actualType}. ${suggestion}`,
|
|
7589
|
+
code: ''
|
|
7616
7590
|
});
|
|
7591
|
+
// Don't check name if it's not a function declaration
|
|
7592
|
+
return violations;
|
|
7617
7593
|
}
|
|
7618
|
-
// Check
|
|
7619
|
-
|
|
7594
|
+
// Check that the function name matches the component name
|
|
7595
|
+
const functionName = firstStatement.id?.name;
|
|
7596
|
+
if (functionName !== componentName) {
|
|
7620
7597
|
violations.push({
|
|
7621
7598
|
rule: 'single-function-only',
|
|
7622
7599
|
severity: 'critical',
|
|
7623
|
-
line:
|
|
7624
|
-
column: 0,
|
|
7625
|
-
message: `Component
|
|
7626
|
-
code: `
|
|
7600
|
+
line: firstStatement.loc?.start.line || 0,
|
|
7601
|
+
column: firstStatement.loc?.start.column || 0,
|
|
7602
|
+
message: `Component function name "${functionName}" does not match component name "${componentName}". The function must be named exactly as specified.`,
|
|
7603
|
+
code: `Rename to: function ${componentName}(...)`
|
|
7604
|
+
});
|
|
7605
|
+
}
|
|
7606
|
+
// Additional check: look for any code before the function that might have been missed
|
|
7607
|
+
// (e.g., leading variable declarations that destructure from React)
|
|
7608
|
+
if (programBody.length === 1 && t.isFunctionDeclaration(firstStatement)) {
|
|
7609
|
+
// Use traverse to find any problematic patterns inside
|
|
7610
|
+
(0, traverse_1.default)(ast, {
|
|
7611
|
+
Program(path) {
|
|
7612
|
+
// Check if there are any directives or other non-obvious code
|
|
7613
|
+
if (path.node.directives && path.node.directives.length > 0) {
|
|
7614
|
+
violations.push({
|
|
7615
|
+
rule: 'single-function-only',
|
|
7616
|
+
severity: 'high',
|
|
7617
|
+
line: 1,
|
|
7618
|
+
column: 0,
|
|
7619
|
+
message: 'Component should not have directives like "use strict". These are added automatically.',
|
|
7620
|
+
code: ''
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
7623
|
+
}
|
|
7627
7624
|
});
|
|
7628
7625
|
}
|
|
7629
7626
|
return violations;
|
|
@@ -8061,6 +8058,44 @@ Correct pattern:
|
|
|
8061
8058
|
});
|
|
8062
8059
|
return violations;
|
|
8063
8060
|
}
|
|
8061
|
+
},
|
|
8062
|
+
{
|
|
8063
|
+
name: 'no-react-destructuring',
|
|
8064
|
+
appliesTo: 'all',
|
|
8065
|
+
test: (ast, componentName, componentSpec) => {
|
|
8066
|
+
const violations = [];
|
|
8067
|
+
(0, traverse_1.default)(ast, {
|
|
8068
|
+
VariableDeclarator(path) {
|
|
8069
|
+
// Check for destructuring from React
|
|
8070
|
+
if (t.isObjectPattern(path.node.id) &&
|
|
8071
|
+
t.isIdentifier(path.node.init) &&
|
|
8072
|
+
path.node.init.name === 'React') {
|
|
8073
|
+
// Get the destructured properties
|
|
8074
|
+
const destructuredProps = path.node.id.properties
|
|
8075
|
+
.filter(prop => t.isObjectProperty(prop) && t.isIdentifier(prop.key))
|
|
8076
|
+
.map(prop => prop.key)
|
|
8077
|
+
.map(key => key.name);
|
|
8078
|
+
violations.push({
|
|
8079
|
+
rule: 'no-react-destructuring',
|
|
8080
|
+
severity: 'critical',
|
|
8081
|
+
line: path.node.loc?.start.line || 0,
|
|
8082
|
+
column: path.node.loc?.start.column || 0,
|
|
8083
|
+
message: `Cannot destructure from React. The hooks (${destructuredProps.join(', ')}) are already available as global functions in the React runtime.`,
|
|
8084
|
+
code: path.toString().substring(0, 100),
|
|
8085
|
+
suggestion: {
|
|
8086
|
+
text: `Remove the destructuring statement. React hooks like ${destructuredProps.join(', ')} are already available globally and don't need to be imported or destructured.`,
|
|
8087
|
+
example: `// Remove this line entirely:
|
|
8088
|
+
// const { ${destructuredProps.join(', ')} } = React;
|
|
8089
|
+
|
|
8090
|
+
// Just use the hooks directly:
|
|
8091
|
+
const [state, setState] = useState(initialValue);`
|
|
8092
|
+
}
|
|
8093
|
+
});
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
});
|
|
8097
|
+
return violations;
|
|
8098
|
+
}
|
|
8064
8099
|
}
|
|
8065
8100
|
];
|
|
8066
8101
|
//# sourceMappingURL=component-linter.js.map
|