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