@memberjunction/react-test-harness 2.94.0 → 2.96.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/lib/component-linter.d.ts +7 -0
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +2099 -112
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +2 -1
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +74 -60
- 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/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
|
|
@@ -91,13 +93,21 @@ function getLineNumber(code, index) {
|
|
|
91
93
|
// These will be evaluated at TypeScript compile time and become static arrays
|
|
92
94
|
const runQueryResultProps = [
|
|
93
95
|
'QueryID', 'QueryName', 'Success', 'Results', 'RowCount',
|
|
94
|
-
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
96
|
+
'TotalRowCount', 'ExecutionTime', 'ErrorMessage', 'AppliedParameters',
|
|
97
|
+
'CacheHit', 'CacheKey', 'CacheTTLRemaining'
|
|
95
98
|
];
|
|
96
99
|
const runViewResultProps = [
|
|
97
100
|
'Success', 'Results', 'UserViewRunID', 'RowCount',
|
|
98
101
|
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
99
102
|
];
|
|
100
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
|
+
}
|
|
101
111
|
// Helper method to check if a statement contains a return
|
|
102
112
|
static containsReturn(node) {
|
|
103
113
|
let hasReturn = false;
|
|
@@ -152,13 +162,78 @@ class ComponentLinter {
|
|
|
152
162
|
}
|
|
153
163
|
return isFromMethod;
|
|
154
164
|
}
|
|
165
|
+
static async validateComponentSyntax(code, componentName) {
|
|
166
|
+
try {
|
|
167
|
+
const parseResult = parser.parse(code, {
|
|
168
|
+
sourceType: 'module',
|
|
169
|
+
plugins: ['jsx', 'typescript'],
|
|
170
|
+
errorRecovery: true,
|
|
171
|
+
ranges: true
|
|
172
|
+
});
|
|
173
|
+
if (parseResult.errors && parseResult.errors.length > 0) {
|
|
174
|
+
const errors = parseResult.errors.map((error) => {
|
|
175
|
+
const location = error.loc ? `Line ${error.loc.line}, Column ${error.loc.column}` : 'Unknown location';
|
|
176
|
+
return `${location}: ${error.message || error.toString()}`;
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
valid: false,
|
|
180
|
+
errors
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
valid: true,
|
|
185
|
+
errors: []
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
// Handle catastrophic parse failures
|
|
190
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
|
|
191
|
+
return {
|
|
192
|
+
valid: false,
|
|
193
|
+
errors: [`Failed to parse component: ${errorMessage}`]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
155
197
|
static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode, options) {
|
|
156
198
|
try {
|
|
157
|
-
|
|
199
|
+
// Parse with error recovery to get both AST and errors
|
|
200
|
+
const parseResult = parser.parse(code, {
|
|
158
201
|
sourceType: 'module',
|
|
159
202
|
plugins: ['jsx', 'typescript'],
|
|
160
|
-
errorRecovery: true
|
|
203
|
+
errorRecovery: true,
|
|
204
|
+
attachComment: false,
|
|
205
|
+
ranges: true,
|
|
206
|
+
tokens: false
|
|
161
207
|
});
|
|
208
|
+
// Check for syntax errors from parser
|
|
209
|
+
const syntaxViolations = [];
|
|
210
|
+
if (parseResult.errors && parseResult.errors.length > 0) {
|
|
211
|
+
for (const error of parseResult.errors) {
|
|
212
|
+
const err = error; // Babel parser errors don't have proper types
|
|
213
|
+
syntaxViolations.push({
|
|
214
|
+
rule: 'syntax-error',
|
|
215
|
+
severity: 'critical',
|
|
216
|
+
line: err.loc?.line || 0,
|
|
217
|
+
column: err.loc?.column || 0,
|
|
218
|
+
message: `Syntax error in component "${componentName}": ${err.message || err.toString()}`,
|
|
219
|
+
code: err.code || 'BABEL_PARSER_ERROR'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// If we have critical syntax errors, return immediately with those
|
|
224
|
+
if (syntaxViolations.length > 0) {
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
violations: syntaxViolations,
|
|
228
|
+
suggestions: this.generateSyntaxErrorSuggestions(syntaxViolations),
|
|
229
|
+
criticalCount: syntaxViolations.length,
|
|
230
|
+
highCount: 0,
|
|
231
|
+
mediumCount: 0,
|
|
232
|
+
lowCount: 0
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// Continue with existing linting logic
|
|
236
|
+
const ast = parseResult;
|
|
162
237
|
// Use universal rules for all components in the new pattern
|
|
163
238
|
let rules = this.universalComponentRules;
|
|
164
239
|
// Filter rules based on component type and appliesTo property
|
|
@@ -1027,6 +1102,32 @@ const { ModelTreeView, PromptTable, FilterPanel } = components;
|
|
|
1027
1102
|
// All these will be available`
|
|
1028
1103
|
});
|
|
1029
1104
|
break;
|
|
1105
|
+
case 'component-usage-without-destructuring':
|
|
1106
|
+
suggestions.push({
|
|
1107
|
+
violation: violation.rule,
|
|
1108
|
+
suggestion: 'Components must be properly accessed - either destructure from components prop or use dot notation',
|
|
1109
|
+
example: `// ❌ WRONG - Using component without destructuring:
|
|
1110
|
+
function MyComponent({ components }) {
|
|
1111
|
+
return <AccountList />; // Error: AccountList not destructured
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ✅ CORRECT - Option 1: Destructure from components
|
|
1115
|
+
function MyComponent({ components }) {
|
|
1116
|
+
const { AccountList } = components;
|
|
1117
|
+
return <AccountList />;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ✅ CORRECT - Option 2: Use dot notation
|
|
1121
|
+
function MyComponent({ components }) {
|
|
1122
|
+
return <components.AccountList />;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// ✅ CORRECT - Option 3: Destructure in function parameters
|
|
1126
|
+
function MyComponent({ components: { AccountList } }) {
|
|
1127
|
+
return <AccountList />;
|
|
1128
|
+
}`
|
|
1129
|
+
});
|
|
1130
|
+
break;
|
|
1030
1131
|
case 'unsafe-array-access':
|
|
1031
1132
|
suggestions.push({
|
|
1032
1133
|
violation: violation.rule,
|
|
@@ -1591,6 +1692,80 @@ setData(queryResult.Results || []); // NOT queryResult directly!
|
|
|
1591
1692
|
// }`
|
|
1592
1693
|
});
|
|
1593
1694
|
break;
|
|
1695
|
+
case 'styles-invalid-path':
|
|
1696
|
+
suggestions.push({
|
|
1697
|
+
violation: violation.rule,
|
|
1698
|
+
suggestion: 'Fix invalid styles property paths. Use the correct ComponentStyles interface structure.',
|
|
1699
|
+
example: `// ❌ WRONG - Invalid property paths:
|
|
1700
|
+
styles.fontSize.small // fontSize is not at root level
|
|
1701
|
+
styles.colors.background // colors.background exists
|
|
1702
|
+
styles.spacing.small // should be styles.spacing.sm
|
|
1703
|
+
|
|
1704
|
+
// ✅ CORRECT - Valid property paths:
|
|
1705
|
+
styles.typography.fontSize.sm // fontSize is under typography
|
|
1706
|
+
styles.colors.background // correct path
|
|
1707
|
+
styles.spacing.sm // correct size name
|
|
1708
|
+
|
|
1709
|
+
// With safe access and fallbacks:
|
|
1710
|
+
styles?.typography?.fontSize?.sm || '14px'
|
|
1711
|
+
styles?.colors?.background || '#FFFFFF'
|
|
1712
|
+
styles?.spacing?.sm || '8px'`
|
|
1713
|
+
});
|
|
1714
|
+
break;
|
|
1715
|
+
case 'styles-unsafe-access':
|
|
1716
|
+
suggestions.push({
|
|
1717
|
+
violation: violation.rule,
|
|
1718
|
+
suggestion: 'Use optional chaining for nested styles access to prevent runtime errors.',
|
|
1719
|
+
example: `// ❌ UNSAFE - Direct nested access:
|
|
1720
|
+
const fontSize = styles.typography.fontSize.md;
|
|
1721
|
+
const borderRadius = styles.borders.radius.sm;
|
|
1722
|
+
|
|
1723
|
+
// ✅ SAFE - With optional chaining and fallbacks:
|
|
1724
|
+
const fontSize = styles?.typography?.fontSize?.md || '14px';
|
|
1725
|
+
const borderRadius = styles?.borders?.radius?.sm || '6px';
|
|
1726
|
+
|
|
1727
|
+
// Even better - destructure with defaults:
|
|
1728
|
+
const {
|
|
1729
|
+
typography: {
|
|
1730
|
+
fontSize: { md: fontSize = '14px' } = {}
|
|
1731
|
+
} = {}
|
|
1732
|
+
} = styles || {};`
|
|
1733
|
+
});
|
|
1734
|
+
break;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return suggestions;
|
|
1738
|
+
}
|
|
1739
|
+
static generateSyntaxErrorSuggestions(violations) {
|
|
1740
|
+
const suggestions = [];
|
|
1741
|
+
for (const violation of violations) {
|
|
1742
|
+
if (violation.message.includes('Unterminated string')) {
|
|
1743
|
+
suggestions.push({
|
|
1744
|
+
violation: violation.rule,
|
|
1745
|
+
suggestion: 'Check that all string literals are properly closed with matching quotes',
|
|
1746
|
+
example: 'Template literals with interpolation must use backticks: `text ${variable} text`'
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
else if (violation.message.includes('Unexpected token') || violation.message.includes('export')) {
|
|
1750
|
+
suggestions.push({
|
|
1751
|
+
violation: violation.rule,
|
|
1752
|
+
suggestion: 'Ensure all code is within the component function body',
|
|
1753
|
+
example: 'Remove any export statements or code outside the function definition'
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
else if (violation.message.includes('import') && violation.message.includes('top level')) {
|
|
1757
|
+
suggestions.push({
|
|
1758
|
+
violation: violation.rule,
|
|
1759
|
+
suggestion: 'Import statements are not allowed in components - use props instead',
|
|
1760
|
+
example: 'Access libraries through props: const { React, MaterialUI } = props.components'
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
else {
|
|
1764
|
+
suggestions.push({
|
|
1765
|
+
violation: violation.rule,
|
|
1766
|
+
suggestion: 'Fix the syntax error before the component can be compiled',
|
|
1767
|
+
example: 'Review the code at the specified line and column for syntax issues'
|
|
1768
|
+
});
|
|
1594
1769
|
}
|
|
1595
1770
|
}
|
|
1596
1771
|
return suggestions;
|
|
@@ -2028,34 +2203,69 @@ ComponentLinter.universalComponentRules = [
|
|
|
2028
2203
|
appliesTo: 'all',
|
|
2029
2204
|
test: (ast, componentName, componentSpec) => {
|
|
2030
2205
|
const violations = [];
|
|
2206
|
+
// Track if we're inside the main function and where it ends
|
|
2207
|
+
let mainFunctionEnd = 0;
|
|
2208
|
+
// First pass: find the main component function
|
|
2209
|
+
(0, traverse_1.default)(ast, {
|
|
2210
|
+
FunctionDeclaration(path) {
|
|
2211
|
+
if (path.node.id?.name === componentName) {
|
|
2212
|
+
mainFunctionEnd = path.node.loc?.end.line || 0;
|
|
2213
|
+
path.stop(); // Stop traversing once we find it
|
|
2214
|
+
}
|
|
2215
|
+
},
|
|
2216
|
+
FunctionExpression(path) {
|
|
2217
|
+
// Check for function expressions assigned to const/let/var
|
|
2218
|
+
const parent = path.parent;
|
|
2219
|
+
if (t.isVariableDeclarator(parent) &&
|
|
2220
|
+
t.isIdentifier(parent.id) &&
|
|
2221
|
+
parent.id.name === componentName) {
|
|
2222
|
+
mainFunctionEnd = path.node.loc?.end.line || 0;
|
|
2223
|
+
path.stop();
|
|
2224
|
+
}
|
|
2225
|
+
},
|
|
2226
|
+
ArrowFunctionExpression(path) {
|
|
2227
|
+
// Check for arrow functions assigned to const/let/var
|
|
2228
|
+
const parent = path.parent;
|
|
2229
|
+
if (t.isVariableDeclarator(parent) &&
|
|
2230
|
+
t.isIdentifier(parent.id) &&
|
|
2231
|
+
parent.id.name === componentName) {
|
|
2232
|
+
mainFunctionEnd = path.node.loc?.end.line || 0;
|
|
2233
|
+
path.stop();
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
// Second pass: check for export statements
|
|
2031
2238
|
(0, traverse_1.default)(ast, {
|
|
2032
2239
|
ExportNamedDeclaration(path) {
|
|
2240
|
+
const line = path.node.loc?.start.line || 0;
|
|
2033
2241
|
violations.push({
|
|
2034
2242
|
rule: 'no-export-statements',
|
|
2035
2243
|
severity: 'critical',
|
|
2036
|
-
line:
|
|
2244
|
+
line: line,
|
|
2037
2245
|
column: path.node.loc?.start.column || 0,
|
|
2038
|
-
message: `Component "${componentName}" contains an export statement. Interactive components are self-contained and cannot export values.`,
|
|
2246
|
+
message: `Component "${componentName}" contains an export statement${mainFunctionEnd > 0 && line > mainFunctionEnd ? ' after the component function' : ''}. Interactive components are self-contained and cannot export values.`,
|
|
2039
2247
|
code: path.toString().substring(0, 100)
|
|
2040
2248
|
});
|
|
2041
2249
|
},
|
|
2042
2250
|
ExportDefaultDeclaration(path) {
|
|
2251
|
+
const line = path.node.loc?.start.line || 0;
|
|
2043
2252
|
violations.push({
|
|
2044
2253
|
rule: 'no-export-statements',
|
|
2045
2254
|
severity: 'critical',
|
|
2046
|
-
line:
|
|
2255
|
+
line: line,
|
|
2047
2256
|
column: path.node.loc?.start.column || 0,
|
|
2048
|
-
message: `Component "${componentName}" contains an export default statement. Interactive components are self-contained and cannot export values.`,
|
|
2257
|
+
message: `Component "${componentName}" contains an export default statement${mainFunctionEnd > 0 && line > mainFunctionEnd ? ' after the component function' : ''}. Interactive components are self-contained and cannot export values.`,
|
|
2049
2258
|
code: path.toString().substring(0, 100)
|
|
2050
2259
|
});
|
|
2051
2260
|
},
|
|
2052
2261
|
ExportAllDeclaration(path) {
|
|
2262
|
+
const line = path.node.loc?.start.line || 0;
|
|
2053
2263
|
violations.push({
|
|
2054
2264
|
rule: 'no-export-statements',
|
|
2055
2265
|
severity: 'critical',
|
|
2056
|
-
line:
|
|
2266
|
+
line: line,
|
|
2057
2267
|
column: path.node.loc?.start.column || 0,
|
|
2058
|
-
message: `Component "${componentName}" contains an export * statement. Interactive components are self-contained and cannot export values.`,
|
|
2268
|
+
message: `Component "${componentName}" contains an export * statement${mainFunctionEnd > 0 && line > mainFunctionEnd ? ' after the component function' : ''}. Interactive components are self-contained and cannot export values.`,
|
|
2059
2269
|
code: path.toString().substring(0, 100)
|
|
2060
2270
|
});
|
|
2061
2271
|
}
|
|
@@ -2394,7 +2604,8 @@ ComponentLinter.universalComponentRules = [
|
|
|
2394
2604
|
const libraryMap = new Map();
|
|
2395
2605
|
if (componentSpec?.libraries) {
|
|
2396
2606
|
for (const lib of componentSpec.libraries) {
|
|
2397
|
-
|
|
2607
|
+
// Skip empty library objects or those without required fields
|
|
2608
|
+
if (lib.globalVariable && lib.name) {
|
|
2398
2609
|
// Store both the library name and globalVariable for lookup
|
|
2399
2610
|
libraryMap.set(lib.name.toLowerCase(), lib.globalVariable);
|
|
2400
2611
|
libraryMap.set(lib.globalVariable.toLowerCase(), lib.globalVariable);
|
|
@@ -2695,8 +2906,11 @@ ComponentLinter.universalComponentRules = [
|
|
|
2695
2906
|
const libraryGlobals = new Map();
|
|
2696
2907
|
if (componentSpec?.libraries) {
|
|
2697
2908
|
for (const lib of componentSpec.libraries) {
|
|
2698
|
-
//
|
|
2699
|
-
|
|
2909
|
+
// Skip empty library objects or those without required fields
|
|
2910
|
+
if (lib.name && lib.globalVariable) {
|
|
2911
|
+
// Store both the exact name and lowercase for comparison
|
|
2912
|
+
libraryGlobals.set(lib.name.toLowerCase(), lib.globalVariable);
|
|
2913
|
+
}
|
|
2700
2914
|
}
|
|
2701
2915
|
}
|
|
2702
2916
|
(0, traverse_1.default)(ast, {
|
|
@@ -2879,6 +3093,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2879
3093
|
// Track JSX element usage
|
|
2880
3094
|
JSXElement(path) {
|
|
2881
3095
|
const openingElement = path.node.openingElement;
|
|
3096
|
+
// Check for direct usage (e.g., <ComponentName>)
|
|
2882
3097
|
if (t.isJSXIdentifier(openingElement.name) && /^[A-Z]/.test(openingElement.name.name)) {
|
|
2883
3098
|
const componentName = openingElement.name.name;
|
|
2884
3099
|
// Only track if it's from our destructured components
|
|
@@ -2886,6 +3101,18 @@ ComponentLinter.universalComponentRules = [
|
|
|
2886
3101
|
componentsUsedInJSX.add(componentName);
|
|
2887
3102
|
}
|
|
2888
3103
|
}
|
|
3104
|
+
// Also check for components.X pattern (e.g., <components.ComponentName>)
|
|
3105
|
+
if (t.isJSXMemberExpression(openingElement.name)) {
|
|
3106
|
+
if (t.isJSXIdentifier(openingElement.name.object) &&
|
|
3107
|
+
openingElement.name.object.name === 'components' &&
|
|
3108
|
+
t.isJSXIdentifier(openingElement.name.property)) {
|
|
3109
|
+
const componentName = openingElement.name.property.name;
|
|
3110
|
+
// Track usage of components accessed via dot notation
|
|
3111
|
+
if (componentsFromProps.has(componentName)) {
|
|
3112
|
+
componentsUsedInJSX.add(componentName);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
2889
3116
|
}
|
|
2890
3117
|
});
|
|
2891
3118
|
// Only check if we found a components prop
|
|
@@ -2905,6 +3132,75 @@ ComponentLinter.universalComponentRules = [
|
|
|
2905
3132
|
return violations;
|
|
2906
3133
|
}
|
|
2907
3134
|
},
|
|
3135
|
+
{
|
|
3136
|
+
name: 'component-not-in-dependencies',
|
|
3137
|
+
appliesTo: 'all',
|
|
3138
|
+
test: (ast, componentName, componentSpec) => {
|
|
3139
|
+
const violations = [];
|
|
3140
|
+
// Get the list of available component names from dependencies
|
|
3141
|
+
const availableComponents = new Set();
|
|
3142
|
+
if (componentSpec?.dependencies) {
|
|
3143
|
+
for (const dep of componentSpec.dependencies) {
|
|
3144
|
+
if (dep.name) {
|
|
3145
|
+
availableComponents.add(dep.name);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
(0, traverse_1.default)(ast, {
|
|
3150
|
+
// Check for components.X usage in JSX
|
|
3151
|
+
JSXElement(path) {
|
|
3152
|
+
const openingElement = path.node.openingElement;
|
|
3153
|
+
// Check for components.X pattern (e.g., <components.Loading>)
|
|
3154
|
+
if (t.isJSXMemberExpression(openingElement.name)) {
|
|
3155
|
+
if (t.isJSXIdentifier(openingElement.name.object) &&
|
|
3156
|
+
openingElement.name.object.name === 'components' &&
|
|
3157
|
+
t.isJSXIdentifier(openingElement.name.property)) {
|
|
3158
|
+
const componentName = openingElement.name.property.name;
|
|
3159
|
+
// Check if this component is NOT in the dependencies
|
|
3160
|
+
if (!availableComponents.has(componentName)) {
|
|
3161
|
+
violations.push({
|
|
3162
|
+
rule: 'component-not-in-dependencies',
|
|
3163
|
+
severity: 'critical',
|
|
3164
|
+
line: openingElement.loc?.start.line || 0,
|
|
3165
|
+
column: openingElement.loc?.start.column || 0,
|
|
3166
|
+
message: `Component "${componentName}" is used via components.${componentName} but is not defined in the component spec's dependencies array. This will cause a runtime error.`,
|
|
3167
|
+
code: `<components.${componentName}>`
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
},
|
|
3173
|
+
// Also check for components.X usage in JavaScript expressions
|
|
3174
|
+
MemberExpression(path) {
|
|
3175
|
+
if (t.isIdentifier(path.node.object) &&
|
|
3176
|
+
path.node.object.name === 'components' &&
|
|
3177
|
+
t.isIdentifier(path.node.property)) {
|
|
3178
|
+
const componentName = path.node.property.name;
|
|
3179
|
+
// Skip if this is a method call like components.hasOwnProperty
|
|
3180
|
+
const parent = path.parent;
|
|
3181
|
+
if (t.isCallExpression(parent) && parent.callee === path.node) {
|
|
3182
|
+
// Check if it looks like a component (starts with uppercase)
|
|
3183
|
+
if (!/^[A-Z]/.test(componentName)) {
|
|
3184
|
+
return; // Skip built-in methods
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
// Check if this component is NOT in the dependencies
|
|
3188
|
+
if (/^[A-Z]/.test(componentName) && !availableComponents.has(componentName)) {
|
|
3189
|
+
violations.push({
|
|
3190
|
+
rule: 'component-not-in-dependencies',
|
|
3191
|
+
severity: 'critical',
|
|
3192
|
+
line: path.node.loc?.start.line || 0,
|
|
3193
|
+
column: path.node.loc?.start.column || 0,
|
|
3194
|
+
message: `Component "${componentName}" is accessed via components.${componentName} but is not defined in the component spec's dependencies array. This will cause a runtime error.`,
|
|
3195
|
+
code: `components.${componentName}`
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
return violations;
|
|
3202
|
+
}
|
|
3203
|
+
},
|
|
2908
3204
|
// DISABLED: Consolidated into unsafe-array-operations rule
|
|
2909
3205
|
// {
|
|
2910
3206
|
// name: 'unsafe-array-access',
|
|
@@ -3697,7 +3993,23 @@ ComponentLinter.universalComponentRules = [
|
|
|
3697
3993
|
severity: 'critical',
|
|
3698
3994
|
line: path.node.arguments[0].loc?.start.line || 0,
|
|
3699
3995
|
column: path.node.arguments[0].loc?.start.column || 0,
|
|
3700
|
-
message: `RunViews expects an array of RunViewParams objects, not a ${t.isObjectExpression(path.node.arguments[0]) ? 'single object' : 'non-array'}.
|
|
3996
|
+
message: `RunViews expects an array of RunViewParams objects, not a ${t.isObjectExpression(path.node.arguments[0]) ? 'single object' : 'non-array'}.
|
|
3997
|
+
Use: RunViews([
|
|
3998
|
+
{
|
|
3999
|
+
EntityName: 'Entity1',
|
|
4000
|
+
ExtraFilter: 'IsActive = 1',
|
|
4001
|
+
Fields: 'ID, Name',
|
|
4002
|
+
StartRow: 0,
|
|
4003
|
+
MaxRows: 50
|
|
4004
|
+
},
|
|
4005
|
+
{
|
|
4006
|
+
EntityName: 'Entity2',
|
|
4007
|
+
OrderBy: 'CreatedAt DESC',
|
|
4008
|
+
StartRow: 0,
|
|
4009
|
+
MaxRows: 100
|
|
4010
|
+
}
|
|
4011
|
+
])
|
|
4012
|
+
Each object supports: EntityName, ExtraFilter, Fields, OrderBy, MaxRows, StartRow, ResultType`,
|
|
3701
4013
|
code: path.toString().substring(0, 100)
|
|
3702
4014
|
});
|
|
3703
4015
|
}
|
|
@@ -3718,7 +4030,16 @@ ComponentLinter.universalComponentRules = [
|
|
|
3718
4030
|
severity: 'critical',
|
|
3719
4031
|
line: path.node.arguments[0].loc?.start.line || 0,
|
|
3720
4032
|
column: path.node.arguments[0].loc?.start.column || 0,
|
|
3721
|
-
message: `RunView expects a RunViewParams object, not ${argType === 'array' ? 'an' : 'a'} ${argType}.
|
|
4033
|
+
message: `RunView expects a RunViewParams object, not ${argType === 'array' ? 'an' : 'a'} ${argType}.
|
|
4034
|
+
Use: RunView({
|
|
4035
|
+
EntityName: 'YourEntity',
|
|
4036
|
+
ExtraFilter: 'Status = "Active"', // Optional WHERE clause
|
|
4037
|
+
Fields: 'ID, Name, Status', // Optional columns to return
|
|
4038
|
+
OrderBy: 'Name ASC', // Optional sort
|
|
4039
|
+
StartRow: 0, // Optional offset (0-based)
|
|
4040
|
+
MaxRows: 100 // Optional limit
|
|
4041
|
+
})
|
|
4042
|
+
Valid properties: EntityName, ExtraFilter, Fields, OrderBy, MaxRows, StartRow, ResultType`,
|
|
3722
4043
|
code: path.toString().substring(0, 100)
|
|
3723
4044
|
});
|
|
3724
4045
|
}
|
|
@@ -3772,6 +4093,117 @@ ComponentLinter.universalComponentRules = [
|
|
|
3772
4093
|
code: `${propName}: ...`
|
|
3773
4094
|
});
|
|
3774
4095
|
}
|
|
4096
|
+
else {
|
|
4097
|
+
// Property name is valid, now check its type
|
|
4098
|
+
const value = prop.value;
|
|
4099
|
+
// Helper to check if a node is null or undefined
|
|
4100
|
+
const isNullOrUndefined = (node) => {
|
|
4101
|
+
return t.isNullLiteral(node) ||
|
|
4102
|
+
(t.isIdentifier(node) && node.name === 'undefined');
|
|
4103
|
+
};
|
|
4104
|
+
// Helper to check if a node could evaluate to a string
|
|
4105
|
+
const isStringLike = (node, depth = 0) => {
|
|
4106
|
+
// Prevent infinite recursion
|
|
4107
|
+
if (depth > 3)
|
|
4108
|
+
return false;
|
|
4109
|
+
// Special handling for ternary operators - check both branches
|
|
4110
|
+
if (t.isConditionalExpression(node)) {
|
|
4111
|
+
const consequentOk = isStringLike(node.consequent, depth + 1) || isNullOrUndefined(node.consequent);
|
|
4112
|
+
const alternateOk = isStringLike(node.alternate, depth + 1) || isNullOrUndefined(node.alternate);
|
|
4113
|
+
return consequentOk && alternateOk;
|
|
4114
|
+
}
|
|
4115
|
+
// Explicitly reject object and array expressions
|
|
4116
|
+
if (t.isObjectExpression(node) || t.isArrayExpression(node)) {
|
|
4117
|
+
return false;
|
|
4118
|
+
}
|
|
4119
|
+
return t.isStringLiteral(node) ||
|
|
4120
|
+
t.isTemplateLiteral(node) ||
|
|
4121
|
+
t.isBinaryExpression(node) || // String concatenation
|
|
4122
|
+
t.isIdentifier(node) || // Variable
|
|
4123
|
+
t.isCallExpression(node) || // Function call
|
|
4124
|
+
t.isMemberExpression(node); // Property access
|
|
4125
|
+
};
|
|
4126
|
+
// Helper to check if a node could evaluate to a number
|
|
4127
|
+
const isNumberLike = (node) => {
|
|
4128
|
+
return t.isNumericLiteral(node) ||
|
|
4129
|
+
t.isBinaryExpression(node) || // Math operations
|
|
4130
|
+
t.isUnaryExpression(node) || // Negative numbers, etc
|
|
4131
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4132
|
+
t.isIdentifier(node) || // Variable
|
|
4133
|
+
t.isCallExpression(node) || // Function call
|
|
4134
|
+
t.isMemberExpression(node); // Property access
|
|
4135
|
+
};
|
|
4136
|
+
// Helper to check if a node is array-like
|
|
4137
|
+
const isArrayLike = (node) => {
|
|
4138
|
+
return t.isArrayExpression(node) ||
|
|
4139
|
+
t.isIdentifier(node) || // Variable
|
|
4140
|
+
t.isCallExpression(node) || // Function returning array
|
|
4141
|
+
t.isMemberExpression(node) || // Property access
|
|
4142
|
+
t.isConditionalExpression(node); // Ternary
|
|
4143
|
+
};
|
|
4144
|
+
// Helper to check if a node is object-like (but not array)
|
|
4145
|
+
const isObjectLike = (node) => {
|
|
4146
|
+
if (t.isArrayExpression(node))
|
|
4147
|
+
return false;
|
|
4148
|
+
return t.isObjectExpression(node) ||
|
|
4149
|
+
t.isIdentifier(node) || // Variable
|
|
4150
|
+
t.isCallExpression(node) || // Function returning object
|
|
4151
|
+
t.isMemberExpression(node) || // Property access
|
|
4152
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4153
|
+
t.isSpreadElement(node); // Spread syntax (though this is the problem case)
|
|
4154
|
+
};
|
|
4155
|
+
// Validate types based on property name
|
|
4156
|
+
if (propName === 'ExtraFilter' || propName === 'OrderBy' || propName === 'EntityName') {
|
|
4157
|
+
// These must be strings (ExtraFilter and OrderBy can also be null/undefined)
|
|
4158
|
+
const allowNullUndefined = propName === 'ExtraFilter' || propName === 'OrderBy';
|
|
4159
|
+
if (!isStringLike(value) && !(allowNullUndefined && isNullOrUndefined(value))) {
|
|
4160
|
+
let exampleValue = '';
|
|
4161
|
+
if (propName === 'ExtraFilter') {
|
|
4162
|
+
exampleValue = `"Status = 'Active' AND Type = 'Customer'"`;
|
|
4163
|
+
}
|
|
4164
|
+
else if (propName === 'OrderBy') {
|
|
4165
|
+
exampleValue = `"CreatedAt DESC"`;
|
|
4166
|
+
}
|
|
4167
|
+
else if (propName === 'EntityName') {
|
|
4168
|
+
exampleValue = `"Products"`;
|
|
4169
|
+
}
|
|
4170
|
+
violations.push({
|
|
4171
|
+
rule: 'runview-runquery-valid-properties',
|
|
4172
|
+
severity: 'critical',
|
|
4173
|
+
line: prop.loc?.start.line || 0,
|
|
4174
|
+
column: prop.loc?.start.column || 0,
|
|
4175
|
+
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}`,
|
|
4176
|
+
code: `${propName}: ${prop.value.type === 'ObjectExpression' ? '{...}' : prop.value.type === 'ArrayExpression' ? '[...]' : '...'}`
|
|
4177
|
+
});
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
else if (propName === 'Fields') {
|
|
4181
|
+
// Fields must be an array of strings (or a string that we'll interpret as comma-separated)
|
|
4182
|
+
if (!isArrayLike(value) && !isStringLike(value)) {
|
|
4183
|
+
violations.push({
|
|
4184
|
+
rule: 'runview-runquery-valid-properties',
|
|
4185
|
+
severity: 'critical',
|
|
4186
|
+
line: prop.loc?.start.line || 0,
|
|
4187
|
+
column: prop.loc?.start.column || 0,
|
|
4188
|
+
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'`,
|
|
4189
|
+
code: `Fields: ${prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
else if (propName === 'MaxRows' || propName === 'StartRow') {
|
|
4194
|
+
// These must be numbers
|
|
4195
|
+
if (!isNumberLike(value)) {
|
|
4196
|
+
violations.push({
|
|
4197
|
+
rule: 'runview-runquery-valid-properties',
|
|
4198
|
+
severity: 'critical',
|
|
4199
|
+
line: prop.loc?.start.line || 0,
|
|
4200
|
+
column: prop.loc?.start.column || 0,
|
|
4201
|
+
message: `${methodName} property '${propName}' must be a number. Example: ${propName}: ${propName === 'MaxRows' ? '100' : '0'}`,
|
|
4202
|
+
code: `${propName}: ${prop.value.type === 'StringLiteral' ? '"..."' : prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
3775
4207
|
}
|
|
3776
4208
|
}
|
|
3777
4209
|
// Check that EntityName is present (required property)
|
|
@@ -3804,7 +4236,15 @@ ComponentLinter.universalComponentRules = [
|
|
|
3804
4236
|
severity: 'critical',
|
|
3805
4237
|
line: path.node.loc?.start.line || 0,
|
|
3806
4238
|
column: path.node.loc?.start.column || 0,
|
|
3807
|
-
message: `RunQuery requires a RunQueryParams object as the first parameter.
|
|
4239
|
+
message: `RunQuery requires a RunQueryParams object as the first parameter.
|
|
4240
|
+
Use: RunQuery({
|
|
4241
|
+
QueryName: 'YourQuery', // Or use QueryID: 'uuid'
|
|
4242
|
+
Parameters: { // Optional query parameters
|
|
4243
|
+
param1: 'value1'
|
|
4244
|
+
},
|
|
4245
|
+
StartRow: 0, // Optional offset (0-based)
|
|
4246
|
+
MaxRows: 100 // Optional limit
|
|
4247
|
+
})`,
|
|
3808
4248
|
code: `RunQuery()`
|
|
3809
4249
|
});
|
|
3810
4250
|
}
|
|
@@ -3818,7 +4258,17 @@ ComponentLinter.universalComponentRules = [
|
|
|
3818
4258
|
severity: 'critical',
|
|
3819
4259
|
line: path.node.arguments[0].loc?.start.line || 0,
|
|
3820
4260
|
column: path.node.arguments[0].loc?.start.column || 0,
|
|
3821
|
-
message: `RunQuery expects a RunQueryParams object, not a ${argType}.
|
|
4261
|
+
message: `RunQuery expects a RunQueryParams object, not a ${argType}.
|
|
4262
|
+
Use: RunQuery({
|
|
4263
|
+
QueryName: 'YourQuery', // Or use QueryID: 'uuid'
|
|
4264
|
+
Parameters: { // Optional query parameters
|
|
4265
|
+
startDate: '2024-01-01',
|
|
4266
|
+
endDate: '2024-12-31'
|
|
4267
|
+
},
|
|
4268
|
+
StartRow: 0, // Optional offset (0-based)
|
|
4269
|
+
MaxRows: 100 // Optional limit
|
|
4270
|
+
})
|
|
4271
|
+
Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxRows, StartRow, ForceAuditLog, AuditLogDescription`,
|
|
3822
4272
|
code: path.toString().substring(0, 100)
|
|
3823
4273
|
});
|
|
3824
4274
|
}
|
|
@@ -3858,6 +4308,111 @@ ComponentLinter.universalComponentRules = [
|
|
|
3858
4308
|
code: `${propName}: ...`
|
|
3859
4309
|
});
|
|
3860
4310
|
}
|
|
4311
|
+
else {
|
|
4312
|
+
// Property name is valid, now check its type
|
|
4313
|
+
const value = prop.value;
|
|
4314
|
+
// Helper to check if a node is null or undefined
|
|
4315
|
+
const isNullOrUndefined = (node) => {
|
|
4316
|
+
return t.isNullLiteral(node) ||
|
|
4317
|
+
(t.isIdentifier(node) && node.name === 'undefined');
|
|
4318
|
+
};
|
|
4319
|
+
// Helper to check if a node could evaluate to a string
|
|
4320
|
+
const isStringLike = (node, depth = 0) => {
|
|
4321
|
+
// Prevent infinite recursion
|
|
4322
|
+
if (depth > 3)
|
|
4323
|
+
return false;
|
|
4324
|
+
// Special handling for ternary operators - check both branches
|
|
4325
|
+
if (t.isConditionalExpression(node)) {
|
|
4326
|
+
const consequentOk = isStringLike(node.consequent, depth + 1) || isNullOrUndefined(node.consequent);
|
|
4327
|
+
const alternateOk = isStringLike(node.alternate, depth + 1) || isNullOrUndefined(node.alternate);
|
|
4328
|
+
return consequentOk && alternateOk;
|
|
4329
|
+
}
|
|
4330
|
+
// Explicitly reject object and array expressions
|
|
4331
|
+
if (t.isObjectExpression(node) || t.isArrayExpression(node)) {
|
|
4332
|
+
return false;
|
|
4333
|
+
}
|
|
4334
|
+
return t.isStringLiteral(node) ||
|
|
4335
|
+
t.isTemplateLiteral(node) ||
|
|
4336
|
+
t.isBinaryExpression(node) || // String concatenation
|
|
4337
|
+
t.isIdentifier(node) || // Variable
|
|
4338
|
+
t.isCallExpression(node) || // Function call
|
|
4339
|
+
t.isMemberExpression(node); // Property access
|
|
4340
|
+
};
|
|
4341
|
+
// Helper to check if a node could evaluate to a number
|
|
4342
|
+
const isNumberLike = (node) => {
|
|
4343
|
+
return t.isNumericLiteral(node) ||
|
|
4344
|
+
t.isBinaryExpression(node) || // Math operations
|
|
4345
|
+
t.isUnaryExpression(node) || // Negative numbers, etc
|
|
4346
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4347
|
+
t.isIdentifier(node) || // Variable
|
|
4348
|
+
t.isCallExpression(node) || // Function call
|
|
4349
|
+
t.isMemberExpression(node); // Property access
|
|
4350
|
+
};
|
|
4351
|
+
// Helper to check if a node is object-like (but not array)
|
|
4352
|
+
const isObjectLike = (node) => {
|
|
4353
|
+
if (t.isArrayExpression(node))
|
|
4354
|
+
return false;
|
|
4355
|
+
return t.isObjectExpression(node) ||
|
|
4356
|
+
t.isIdentifier(node) || // Variable
|
|
4357
|
+
t.isCallExpression(node) || // Function returning object
|
|
4358
|
+
t.isMemberExpression(node) || // Property access
|
|
4359
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4360
|
+
t.isSpreadElement(node); // Spread syntax
|
|
4361
|
+
};
|
|
4362
|
+
// Validate types based on property name
|
|
4363
|
+
if (propName === 'QueryID' || propName === 'QueryName' || propName === 'CategoryID' || propName === 'CategoryPath') {
|
|
4364
|
+
// These must be strings
|
|
4365
|
+
if (!isStringLike(value)) {
|
|
4366
|
+
let exampleValue = '';
|
|
4367
|
+
if (propName === 'QueryID') {
|
|
4368
|
+
exampleValue = `"550e8400-e29b-41d4-a716-446655440000"`;
|
|
4369
|
+
}
|
|
4370
|
+
else if (propName === 'QueryName') {
|
|
4371
|
+
exampleValue = `"Sales by Region"`;
|
|
4372
|
+
}
|
|
4373
|
+
else if (propName === 'CategoryID') {
|
|
4374
|
+
exampleValue = `"123e4567-e89b-12d3-a456-426614174000"`;
|
|
4375
|
+
}
|
|
4376
|
+
else if (propName === 'CategoryPath') {
|
|
4377
|
+
exampleValue = `"/Reports/Sales/"`;
|
|
4378
|
+
}
|
|
4379
|
+
violations.push({
|
|
4380
|
+
rule: 'runview-runquery-valid-properties',
|
|
4381
|
+
severity: 'critical',
|
|
4382
|
+
line: prop.loc?.start.line || 0,
|
|
4383
|
+
column: prop.loc?.start.column || 0,
|
|
4384
|
+
message: `RunQuery property '${propName}' must be a string. Example: ${propName}: ${exampleValue}`,
|
|
4385
|
+
code: `${propName}: ${prop.value.type === 'ObjectExpression' ? '{...}' : prop.value.type === 'ArrayExpression' ? '[...]' : '...'}`
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
else if (propName === 'Parameters') {
|
|
4390
|
+
// Parameters must be an object (Record<string, any>)
|
|
4391
|
+
if (!isObjectLike(value)) {
|
|
4392
|
+
violations.push({
|
|
4393
|
+
rule: 'runview-runquery-valid-properties',
|
|
4394
|
+
severity: 'critical',
|
|
4395
|
+
line: prop.loc?.start.line || 0,
|
|
4396
|
+
column: prop.loc?.start.column || 0,
|
|
4397
|
+
message: `RunQuery property 'Parameters' must be an object containing key-value pairs. Example: Parameters: { startDate: '2024-01-01', status: 'Active' }`,
|
|
4398
|
+
code: `Parameters: ${t.isArrayExpression(value) ? '[...]' : t.isStringLiteral(value) ? '"..."' : '...'}`
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
else if (propName === 'MaxRows' || propName === 'StartRow') {
|
|
4403
|
+
// These must be numbers
|
|
4404
|
+
if (!isNumberLike(value)) {
|
|
4405
|
+
violations.push({
|
|
4406
|
+
rule: 'runview-runquery-valid-properties',
|
|
4407
|
+
severity: 'critical',
|
|
4408
|
+
line: prop.loc?.start.line || 0,
|
|
4409
|
+
column: prop.loc?.start.column || 0,
|
|
4410
|
+
message: `RunQuery property '${propName}' must be a number. Example: ${propName}: ${propName === 'MaxRows' ? '100' : '0'}`,
|
|
4411
|
+
code: `${propName}: ${prop.value.type === 'StringLiteral' ? '"..."' : prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
3861
4416
|
}
|
|
3862
4417
|
}
|
|
3863
4418
|
// Check that at least one required property is present
|
|
@@ -4394,8 +4949,10 @@ ComponentLinter.universalComponentRules = [
|
|
|
4394
4949
|
test: (ast, componentName, componentSpec) => {
|
|
4395
4950
|
const violations = [];
|
|
4396
4951
|
const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
|
|
4397
|
-
//
|
|
4398
|
-
const
|
|
4952
|
+
// React special props that are automatically provided by React
|
|
4953
|
+
const reactSpecialProps = new Set(['children']);
|
|
4954
|
+
// Build set of allowed props: standard props + React special props + componentSpec properties
|
|
4955
|
+
const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
|
|
4399
4956
|
// Add props from componentSpec.properties if they exist
|
|
4400
4957
|
if (componentSpec?.properties) {
|
|
4401
4958
|
for (const prop of componentSpec.properties) {
|
|
@@ -4430,7 +4987,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
4430
4987
|
severity: 'critical',
|
|
4431
4988
|
line: path.node.loc?.start.line || 0,
|
|
4432
4989
|
column: path.node.loc?.start.column || 0,
|
|
4433
|
-
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
4990
|
+
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}, React special props: ${Array.from(reactSpecialProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
4434
4991
|
});
|
|
4435
4992
|
}
|
|
4436
4993
|
}
|
|
@@ -4463,9 +5020,231 @@ ComponentLinter.universalComponentRules = [
|
|
|
4463
5020
|
severity: 'critical',
|
|
4464
5021
|
line: path.node.loc?.start.line || 0,
|
|
4465
5022
|
column: path.node.loc?.start.column || 0,
|
|
4466
|
-
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
5023
|
+
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}, React special props: ${Array.from(reactSpecialProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
5024
|
+
});
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
});
|
|
5031
|
+
return violations;
|
|
5032
|
+
}
|
|
5033
|
+
},
|
|
5034
|
+
{
|
|
5035
|
+
name: 'validate-dependency-props',
|
|
5036
|
+
appliesTo: 'all',
|
|
5037
|
+
test: (ast, componentName, componentSpec) => {
|
|
5038
|
+
const violations = [];
|
|
5039
|
+
// Build a map of dependency components to their specs
|
|
5040
|
+
const dependencySpecs = new Map();
|
|
5041
|
+
// Process embedded dependencies
|
|
5042
|
+
if (componentSpec?.dependencies && Array.isArray(componentSpec.dependencies)) {
|
|
5043
|
+
for (const dep of componentSpec.dependencies) {
|
|
5044
|
+
if (dep && dep.name) {
|
|
5045
|
+
if (dep.location === 'registry') {
|
|
5046
|
+
const match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace, dep.registry);
|
|
5047
|
+
if (!match) {
|
|
5048
|
+
// the specified registry component was not found, we can't lint for it, but we should put a warning
|
|
5049
|
+
console.warn('Dependency component not found in registry', dep);
|
|
5050
|
+
}
|
|
5051
|
+
else {
|
|
5052
|
+
dependencySpecs.set(dep.name, match.spec);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
else {
|
|
5056
|
+
// Embedded dependencies have their spec inline
|
|
5057
|
+
dependencySpecs.set(dep.name, dep);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
else {
|
|
5061
|
+
// we have an invalid dep in the spec, not a fatal error but we should log this
|
|
5062
|
+
console.warn(`Invalid dependency in component spec`, dep);
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
// For registry dependencies, we'd need ComponentMetadataEngine
|
|
5067
|
+
// But since this is a static lint check, we'll focus on embedded deps
|
|
5068
|
+
// Registry components would need async loading which doesn't fit the current sync pattern
|
|
5069
|
+
// Now traverse JSX to find component usage
|
|
5070
|
+
(0, traverse_1.default)(ast, {
|
|
5071
|
+
JSXElement(path) {
|
|
5072
|
+
const openingElement = path.node.openingElement;
|
|
5073
|
+
// Check if this is one of our dependency components
|
|
5074
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
5075
|
+
const componentName = openingElement.name.name;
|
|
5076
|
+
const depSpec = dependencySpecs.get(componentName);
|
|
5077
|
+
if (depSpec) {
|
|
5078
|
+
// Collect props being passed
|
|
5079
|
+
const passedProps = new Set();
|
|
5080
|
+
const passedPropNodes = new Map();
|
|
5081
|
+
for (const attr of openingElement.attributes) {
|
|
5082
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
5083
|
+
const propName = attr.name.name;
|
|
5084
|
+
passedProps.add(propName);
|
|
5085
|
+
passedPropNodes.set(propName, attr);
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
// Check required custom props
|
|
5089
|
+
if (depSpec.properties && Array.isArray(depSpec.properties)) {
|
|
5090
|
+
const requiredProps = [];
|
|
5091
|
+
const optionalProps = [];
|
|
5092
|
+
for (const prop of depSpec.properties) {
|
|
5093
|
+
if (prop && prop.name && typeof prop.name === 'string') {
|
|
5094
|
+
if (prop.required === true) {
|
|
5095
|
+
requiredProps.push(prop.name);
|
|
5096
|
+
}
|
|
5097
|
+
else {
|
|
5098
|
+
optionalProps.push(prop.name);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
// Check for missing required props
|
|
5103
|
+
const missingRequired = requiredProps.filter(prop => {
|
|
5104
|
+
// Special handling for 'children' prop
|
|
5105
|
+
if (prop === 'children') {
|
|
5106
|
+
// Check if JSX element has children nodes
|
|
5107
|
+
const hasChildren = path.node.children && path.node.children.length > 0 &&
|
|
5108
|
+
path.node.children.some(child => !t.isJSXText(child) || (t.isJSXText(child) && child.value.trim() !== ''));
|
|
5109
|
+
return !passedProps.has(prop) && !hasChildren;
|
|
5110
|
+
}
|
|
5111
|
+
return !passedProps.has(prop);
|
|
5112
|
+
});
|
|
5113
|
+
// Separate children warnings from other critical props
|
|
5114
|
+
const missingChildren = missingRequired.filter(prop => prop === 'children');
|
|
5115
|
+
const missingOtherProps = missingRequired.filter(prop => prop !== 'children');
|
|
5116
|
+
// Critical violation for non-children required props
|
|
5117
|
+
if (missingOtherProps.length > 0) {
|
|
5118
|
+
violations.push({
|
|
5119
|
+
rule: 'validate-dependency-props',
|
|
5120
|
+
severity: 'critical',
|
|
5121
|
+
line: openingElement.loc?.start.line || 0,
|
|
5122
|
+
column: openingElement.loc?.start.column || 0,
|
|
5123
|
+
message: `Dependency component "${componentName}" is missing required props: ${missingOtherProps.join(', ')}. These props are marked as required in the component's specification.`,
|
|
5124
|
+
code: `<${componentName} ... />`
|
|
5125
|
+
});
|
|
5126
|
+
}
|
|
5127
|
+
// Medium severity warning for missing children when required
|
|
5128
|
+
if (missingChildren.length > 0) {
|
|
5129
|
+
violations.push({
|
|
5130
|
+
rule: 'validate-dependency-props',
|
|
5131
|
+
severity: 'medium',
|
|
5132
|
+
line: openingElement.loc?.start.line || 0,
|
|
5133
|
+
column: openingElement.loc?.start.column || 0,
|
|
5134
|
+
message: `Component "${componentName}" expects children but none were provided. The 'children' prop is marked as required in the component's specification.`,
|
|
5135
|
+
code: `<${componentName} ... />`
|
|
4467
5136
|
});
|
|
4468
5137
|
}
|
|
5138
|
+
// Validate prop types for passed props
|
|
5139
|
+
for (const [propName, attrNode] of passedPropNodes) {
|
|
5140
|
+
const propSpec = depSpec.properties.find(p => p.name === propName);
|
|
5141
|
+
if (propSpec && propSpec.type) {
|
|
5142
|
+
const value = attrNode.value;
|
|
5143
|
+
// Type validation based on prop spec type
|
|
5144
|
+
if (propSpec.type === 'string') {
|
|
5145
|
+
// Check if value could be a string
|
|
5146
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5147
|
+
const expr = value.expression;
|
|
5148
|
+
// Check for obvious non-string types
|
|
5149
|
+
if (t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) ||
|
|
5150
|
+
t.isArrayExpression(expr) || (t.isObjectExpression(expr) && !t.isTemplateLiteral(expr))) {
|
|
5151
|
+
violations.push({
|
|
5152
|
+
rule: 'validate-dependency-props',
|
|
5153
|
+
severity: 'high',
|
|
5154
|
+
line: attrNode.loc?.start.line || 0,
|
|
5155
|
+
column: attrNode.loc?.start.column || 0,
|
|
5156
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "string" but received a different type.`,
|
|
5157
|
+
code: `${propName}={...}`
|
|
5158
|
+
});
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
else if (propSpec.type === 'number') {
|
|
5163
|
+
// Check if value could be a number
|
|
5164
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5165
|
+
const expr = value.expression;
|
|
5166
|
+
if (t.isStringLiteral(expr) || t.isBooleanLiteral(expr) ||
|
|
5167
|
+
t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
|
|
5168
|
+
violations.push({
|
|
5169
|
+
rule: 'validate-dependency-props',
|
|
5170
|
+
severity: 'high',
|
|
5171
|
+
line: attrNode.loc?.start.line || 0,
|
|
5172
|
+
column: attrNode.loc?.start.column || 0,
|
|
5173
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "number" but received a different type.`,
|
|
5174
|
+
code: `${propName}={...}`
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
else if (propSpec.type === 'boolean') {
|
|
5180
|
+
// Check if value could be a boolean
|
|
5181
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5182
|
+
const expr = value.expression;
|
|
5183
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
|
|
5184
|
+
t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
|
|
5185
|
+
violations.push({
|
|
5186
|
+
rule: 'validate-dependency-props',
|
|
5187
|
+
severity: 'high',
|
|
5188
|
+
line: attrNode.loc?.start.line || 0,
|
|
5189
|
+
column: attrNode.loc?.start.column || 0,
|
|
5190
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "boolean" but received a different type.`,
|
|
5191
|
+
code: `${propName}={...}`
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
else if (propSpec.type === 'array') {
|
|
5197
|
+
// Check if value could be an array
|
|
5198
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5199
|
+
const expr = value.expression;
|
|
5200
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
|
|
5201
|
+
t.isBooleanLiteral(expr) || (t.isObjectExpression(expr) && !t.isArrayExpression(expr))) {
|
|
5202
|
+
violations.push({
|
|
5203
|
+
rule: 'validate-dependency-props',
|
|
5204
|
+
severity: 'high',
|
|
5205
|
+
line: attrNode.loc?.start.line || 0,
|
|
5206
|
+
column: attrNode.loc?.start.column || 0,
|
|
5207
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "array" but received a different type.`,
|
|
5208
|
+
code: `${propName}={...}`
|
|
5209
|
+
});
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
else if (propSpec.type === 'object') {
|
|
5214
|
+
// Check if value could be an object
|
|
5215
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5216
|
+
const expr = value.expression;
|
|
5217
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
|
|
5218
|
+
t.isBooleanLiteral(expr) || t.isArrayExpression(expr)) {
|
|
5219
|
+
violations.push({
|
|
5220
|
+
rule: 'validate-dependency-props',
|
|
5221
|
+
severity: 'high',
|
|
5222
|
+
line: attrNode.loc?.start.line || 0,
|
|
5223
|
+
column: attrNode.loc?.start.column || 0,
|
|
5224
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "object" but received a different type.`,
|
|
5225
|
+
code: `${propName}={...}`
|
|
5226
|
+
});
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
// Check for unknown props (props not in the spec)
|
|
5233
|
+
const specPropNames = new Set(depSpec.properties.map(p => p.name).filter(Boolean));
|
|
5234
|
+
const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
|
|
5235
|
+
const reactSpecialProps = new Set(['children']);
|
|
5236
|
+
for (const passedProp of passedProps) {
|
|
5237
|
+
if (!specPropNames.has(passedProp) && !standardProps.has(passedProp) && !reactSpecialProps.has(passedProp)) {
|
|
5238
|
+
violations.push({
|
|
5239
|
+
rule: 'validate-dependency-props',
|
|
5240
|
+
severity: 'medium',
|
|
5241
|
+
line: passedPropNodes.get(passedProp)?.loc?.start.line || 0,
|
|
5242
|
+
column: passedPropNodes.get(passedProp)?.loc?.start.column || 0,
|
|
5243
|
+
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'}.`,
|
|
5244
|
+
code: `${passedProp}={...}`
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
4469
5248
|
}
|
|
4470
5249
|
}
|
|
4471
5250
|
}
|
|
@@ -4776,13 +5555,12 @@ ComponentLinter.universalComponentRules = [
|
|
|
4776
5555
|
}
|
|
4777
5556
|
}
|
|
4778
5557
|
|
|
4779
|
-
// Track dependency components (
|
|
5558
|
+
// Track dependency components (NOT auto-destructured - must be manually destructured or accessed via components.X)
|
|
4780
5559
|
if (componentSpec?.dependencies) {
|
|
4781
5560
|
for (const dep of componentSpec.dependencies) {
|
|
4782
5561
|
if (dep.name) {
|
|
4783
5562
|
componentsFromProp.add(dep.name);
|
|
4784
|
-
//
|
|
4785
|
-
availableIdentifiers.add(dep.name);
|
|
5563
|
+
// DO NOT add to availableIdentifiers - components must be destructured first
|
|
4786
5564
|
}
|
|
4787
5565
|
}
|
|
4788
5566
|
}
|
|
@@ -5132,88 +5910,56 @@ ComponentLinter.universalComponentRules = [
|
|
|
5132
5910
|
appliesTo: 'all',
|
|
5133
5911
|
test: (ast, componentName, componentSpec) => {
|
|
5134
5912
|
const violations = [];
|
|
5135
|
-
//
|
|
5136
|
-
const
|
|
5913
|
+
// Array methods that would fail on a result object - keep for smart error detection
|
|
5914
|
+
const arrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'sort', 'concat'];
|
|
5137
5915
|
(0, traverse_1.default)(ast, {
|
|
5138
|
-
//
|
|
5139
|
-
AwaitExpression(path) {
|
|
5140
|
-
const callExpr = path.node.argument;
|
|
5141
|
-
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
5142
|
-
const callee = callExpr.callee;
|
|
5143
|
-
// Check for utilities.rv.RunView or utilities.rq.RunQuery pattern
|
|
5144
|
-
if (t.isMemberExpression(callee.object) &&
|
|
5145
|
-
t.isIdentifier(callee.object.object) &&
|
|
5146
|
-
callee.object.object.name === 'utilities') {
|
|
5147
|
-
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
5148
|
-
const isRunView = method === 'RunView' || method === 'RunViews';
|
|
5149
|
-
const isRunQuery = method === 'RunQuery';
|
|
5150
|
-
if (isRunView || isRunQuery) {
|
|
5151
|
-
// Check if this is being assigned to a variable
|
|
5152
|
-
const parent = path.parent;
|
|
5153
|
-
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
5154
|
-
// const result = await utilities.rv.RunView(...)
|
|
5155
|
-
resultVariables.set(parent.id.name, {
|
|
5156
|
-
line: parent.id.loc?.start.line || 0,
|
|
5157
|
-
column: parent.id.loc?.start.column || 0,
|
|
5158
|
-
method: isRunView ? 'RunView' : 'RunQuery'
|
|
5159
|
-
});
|
|
5160
|
-
}
|
|
5161
|
-
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
5162
|
-
// result = await utilities.rv.RunView(...)
|
|
5163
|
-
resultVariables.set(parent.left.name, {
|
|
5164
|
-
line: parent.left.loc?.start.line || 0,
|
|
5165
|
-
column: parent.left.loc?.start.column || 0,
|
|
5166
|
-
method: isRunView ? 'RunView' : 'RunQuery'
|
|
5167
|
-
});
|
|
5168
|
-
}
|
|
5169
|
-
}
|
|
5170
|
-
}
|
|
5171
|
-
}
|
|
5172
|
-
}
|
|
5173
|
-
});
|
|
5174
|
-
// Second pass: check for misuse of these result variables
|
|
5175
|
-
(0, traverse_1.default)(ast, {
|
|
5176
|
-
// Check for direct array operations
|
|
5916
|
+
// Check for direct array operations on RunView/RunQuery results
|
|
5177
5917
|
CallExpression(path) {
|
|
5178
|
-
// Check for array methods being called on result objects
|
|
5179
5918
|
if (t.isMemberExpression(path.node.callee) &&
|
|
5180
5919
|
t.isIdentifier(path.node.callee.object) &&
|
|
5181
5920
|
t.isIdentifier(path.node.callee.property)) {
|
|
5182
5921
|
const objName = path.node.callee.object.name;
|
|
5183
5922
|
const methodName = path.node.callee.property.name;
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
const
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5923
|
+
if (arrayMethods.includes(methodName)) {
|
|
5924
|
+
// Use proper variable tracing instead of naive name matching
|
|
5925
|
+
const isFromRunView = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
|
|
5926
|
+
const isFromRunQuery = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
|
|
5927
|
+
if (isFromRunView || isFromRunQuery) {
|
|
5928
|
+
const methodType = isFromRunView ? 'RunView' : 'RunQuery';
|
|
5929
|
+
const ruleName = isFromRunView ? 'runview-result-invalid-usage' : 'runquery-result-invalid-usage';
|
|
5930
|
+
violations.push({
|
|
5931
|
+
rule: ruleName,
|
|
5932
|
+
severity: 'critical',
|
|
5933
|
+
line: path.node.loc?.start.line || 0,
|
|
5934
|
+
column: path.node.loc?.start.column || 0,
|
|
5935
|
+
message: `Cannot call array method "${methodName}" directly on ${methodType} result. Use "${objName}.Results.${methodName}(...)" instead. ${methodType} returns an object with { Success, Results, ... }, not an array.`,
|
|
5936
|
+
code: `${objName}.${methodName}(...)`
|
|
5937
|
+
});
|
|
5938
|
+
}
|
|
5196
5939
|
}
|
|
5197
5940
|
}
|
|
5198
5941
|
},
|
|
5199
5942
|
// Check for direct usage in setState or as function arguments
|
|
5200
5943
|
Identifier(path) {
|
|
5201
5944
|
const varName = path.node.name;
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5945
|
+
const parent = path.parent;
|
|
5946
|
+
// Use proper variable tracing
|
|
5947
|
+
const isFromRunView = ComponentLinter.isVariableFromRunQueryOrView(path, varName, 'RunView');
|
|
5948
|
+
const isFromRunQuery = ComponentLinter.isVariableFromRunQueryOrView(path, varName, 'RunQuery');
|
|
5949
|
+
if (isFromRunView || isFromRunQuery) {
|
|
5950
|
+
const methodType = isFromRunView ? 'RunView' : 'RunQuery';
|
|
5951
|
+
const ruleName = isFromRunView ? 'runview-result-invalid-usage' : 'runquery-result-invalid-usage';
|
|
5205
5952
|
// Check if being passed to setState-like functions
|
|
5206
5953
|
if (t.isCallExpression(parent) && path.node === parent.arguments[0]) {
|
|
5207
5954
|
const callee = parent.callee;
|
|
5208
5955
|
// Check for setState patterns
|
|
5209
5956
|
if (t.isIdentifier(callee) && /^set[A-Z]/.test(callee.name)) {
|
|
5210
|
-
// Likely a setState function
|
|
5211
5957
|
violations.push({
|
|
5212
|
-
rule:
|
|
5958
|
+
rule: ruleName,
|
|
5213
5959
|
severity: 'critical',
|
|
5214
5960
|
line: path.node.loc?.start.line || 0,
|
|
5215
5961
|
column: path.node.loc?.start.column || 0,
|
|
5216
|
-
message: `Passing ${
|
|
5962
|
+
message: `Passing ${methodType} result directly to setState. Use "${varName}.Results" or check "${varName}.Success" first. ${methodType} returns { Success, Results, ErrorMessage }, not the data array.`,
|
|
5217
5963
|
code: `${callee.name}(${varName})`
|
|
5218
5964
|
});
|
|
5219
5965
|
}
|
|
@@ -5222,11 +5968,11 @@ ComponentLinter.universalComponentRules = [
|
|
|
5222
5968
|
const methodName = callee.property.name;
|
|
5223
5969
|
if (methodName === 'concat' || methodName === 'push' || methodName === 'unshift') {
|
|
5224
5970
|
violations.push({
|
|
5225
|
-
rule:
|
|
5971
|
+
rule: ruleName,
|
|
5226
5972
|
severity: 'critical',
|
|
5227
5973
|
line: path.node.loc?.start.line || 0,
|
|
5228
5974
|
column: path.node.loc?.start.column || 0,
|
|
5229
|
-
message: `Passing ${
|
|
5975
|
+
message: `Passing ${methodType} result to array method. Use "${varName}.Results" instead of "${varName}".`,
|
|
5230
5976
|
code: `...${methodName}(${varName})`
|
|
5231
5977
|
});
|
|
5232
5978
|
}
|
|
@@ -5243,16 +5989,50 @@ ComponentLinter.universalComponentRules = [
|
|
|
5243
5989
|
// Pattern: Array.isArray(result) ? result : []
|
|
5244
5990
|
if (parent.test.arguments[0] === path.node && parent.consequent === path.node) {
|
|
5245
5991
|
violations.push({
|
|
5246
|
-
rule:
|
|
5992
|
+
rule: ruleName,
|
|
5247
5993
|
severity: 'high',
|
|
5248
5994
|
line: path.node.loc?.start.line || 0,
|
|
5249
5995
|
column: path.node.loc?.start.column || 0,
|
|
5250
|
-
message: `${
|
|
5996
|
+
message: `${methodType} result is never an array. Use "${varName}.Results || []" instead of "Array.isArray(${varName}) ? ${varName} : []".`,
|
|
5251
5997
|
code: `Array.isArray(${varName}) ? ${varName} : []`
|
|
5252
5998
|
});
|
|
5253
5999
|
}
|
|
5254
6000
|
}
|
|
5255
6001
|
}
|
|
6002
|
+
},
|
|
6003
|
+
// Check for invalid property access on RunView/RunQuery results
|
|
6004
|
+
MemberExpression(path) {
|
|
6005
|
+
if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
|
|
6006
|
+
const objName = path.node.object.name;
|
|
6007
|
+
const propName = path.node.property.name;
|
|
6008
|
+
// Use proper variable tracing
|
|
6009
|
+
const isFromRunView = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
|
|
6010
|
+
const isFromRunQuery = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
|
|
6011
|
+
if (isFromRunView || isFromRunQuery) {
|
|
6012
|
+
const isValidViewProp = runViewResultProps.includes(propName);
|
|
6013
|
+
const isValidQueryProp = runQueryResultProps.includes(propName);
|
|
6014
|
+
if (isFromRunQuery && !isValidQueryProp) {
|
|
6015
|
+
violations.push({
|
|
6016
|
+
rule: 'runquery-result-invalid-property',
|
|
6017
|
+
severity: 'critical',
|
|
6018
|
+
line: path.node.loc?.start.line || 0,
|
|
6019
|
+
column: path.node.loc?.start.column || 0,
|
|
6020
|
+
message: `Invalid property "${propName}" on RunQuery result. Valid properties: ${runQueryResultProps.join(', ')}.`,
|
|
6021
|
+
code: `${objName}.${propName}`
|
|
6022
|
+
});
|
|
6023
|
+
}
|
|
6024
|
+
else if (isFromRunView && !isValidViewProp) {
|
|
6025
|
+
violations.push({
|
|
6026
|
+
rule: 'runview-result-invalid-property',
|
|
6027
|
+
severity: 'critical',
|
|
6028
|
+
line: path.node.loc?.start.line || 0,
|
|
6029
|
+
column: path.node.loc?.start.column || 0,
|
|
6030
|
+
message: `Invalid property "${propName}" on RunView result. Valid properties: ${runViewResultProps.join(', ')}.`,
|
|
6031
|
+
code: `${objName}.${propName}`
|
|
6032
|
+
});
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
5256
6036
|
}
|
|
5257
6037
|
});
|
|
5258
6038
|
return violations;
|
|
@@ -5263,31 +6043,9 @@ ComponentLinter.universalComponentRules = [
|
|
|
5263
6043
|
appliesTo: 'all',
|
|
5264
6044
|
test: (ast, componentName, componentSpec) => {
|
|
5265
6045
|
const violations = [];
|
|
5266
|
-
//
|
|
5267
|
-
const validRunQueryResultProps = new Set(
|
|
5268
|
-
|
|
5269
|
-
'QueryName', // string
|
|
5270
|
-
'Success', // boolean
|
|
5271
|
-
'Results', // any[]
|
|
5272
|
-
'RowCount', // number
|
|
5273
|
-
'TotalRowCount', // number
|
|
5274
|
-
'ExecutionTime', // number
|
|
5275
|
-
'ErrorMessage', // string
|
|
5276
|
-
'AppliedParameters', // Record<string, any> (optional)
|
|
5277
|
-
'CacheHit', // boolean (optional)
|
|
5278
|
-
'CacheKey', // string (optional)
|
|
5279
|
-
'CacheTTLRemaining' // number (optional)
|
|
5280
|
-
]);
|
|
5281
|
-
// Valid properties for RunViewResult based on MJCore type definition
|
|
5282
|
-
const validRunViewResultProps = new Set([
|
|
5283
|
-
'Success', // boolean
|
|
5284
|
-
'Results', // Array<T>
|
|
5285
|
-
'UserViewRunID', // string (optional)
|
|
5286
|
-
'RowCount', // number
|
|
5287
|
-
'TotalRowCount', // number
|
|
5288
|
-
'ExecutionTime', // number
|
|
5289
|
-
'ErrorMessage' // string
|
|
5290
|
-
]);
|
|
6046
|
+
// Use shared property arrays from top of file - ensures consistency
|
|
6047
|
+
const validRunQueryResultProps = new Set(runQueryResultProps);
|
|
6048
|
+
const validRunViewResultProps = new Set(runViewResultProps);
|
|
5291
6049
|
// Map of common incorrect properties to the correct property
|
|
5292
6050
|
const incorrectToCorrectMap = {
|
|
5293
6051
|
'data': 'Results',
|
|
@@ -5493,6 +6251,197 @@ ComponentLinter.universalComponentRules = [
|
|
|
5493
6251
|
return violations;
|
|
5494
6252
|
}
|
|
5495
6253
|
},
|
|
6254
|
+
{
|
|
6255
|
+
name: 'validate-runview-runquery-result-access',
|
|
6256
|
+
appliesTo: 'all',
|
|
6257
|
+
test: (ast, componentName, componentSpec) => {
|
|
6258
|
+
const violations = [];
|
|
6259
|
+
// Track variables that hold RunView/RunQuery results with their actual names
|
|
6260
|
+
const resultVariables = new Map();
|
|
6261
|
+
// First pass: identify all RunView/RunQuery calls and their assigned variables
|
|
6262
|
+
(0, traverse_1.default)(ast, {
|
|
6263
|
+
AwaitExpression(path) {
|
|
6264
|
+
const callExpr = path.node.argument;
|
|
6265
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
6266
|
+
const callee = callExpr.callee;
|
|
6267
|
+
// Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
|
|
6268
|
+
if (t.isMemberExpression(callee.object) &&
|
|
6269
|
+
t.isIdentifier(callee.object.object) &&
|
|
6270
|
+
callee.object.object.name === 'utilities' &&
|
|
6271
|
+
t.isIdentifier(callee.object.property)) {
|
|
6272
|
+
const subObject = callee.object.property.name;
|
|
6273
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
6274
|
+
let methodType = null;
|
|
6275
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
6276
|
+
methodType = method;
|
|
6277
|
+
}
|
|
6278
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
6279
|
+
methodType = 'RunQuery';
|
|
6280
|
+
}
|
|
6281
|
+
if (methodType) {
|
|
6282
|
+
// Check if this is being assigned to a variable
|
|
6283
|
+
const parent = path.parent;
|
|
6284
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
6285
|
+
// const result = await utilities.rv.RunView(...)
|
|
6286
|
+
resultVariables.set(parent.id.name, {
|
|
6287
|
+
line: parent.id.loc?.start.line || 0,
|
|
6288
|
+
column: parent.id.loc?.start.column || 0,
|
|
6289
|
+
method: methodType,
|
|
6290
|
+
varName: parent.id.name
|
|
6291
|
+
});
|
|
6292
|
+
}
|
|
6293
|
+
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
6294
|
+
// result = await utilities.rv.RunView(...)
|
|
6295
|
+
resultVariables.set(parent.left.name, {
|
|
6296
|
+
line: parent.left.loc?.start.line || 0,
|
|
6297
|
+
column: parent.left.loc?.start.column || 0,
|
|
6298
|
+
method: methodType,
|
|
6299
|
+
varName: parent.left.name
|
|
6300
|
+
});
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
});
|
|
6307
|
+
// Second pass: check for incorrect usage patterns
|
|
6308
|
+
(0, traverse_1.default)(ast, {
|
|
6309
|
+
// Check for .length property access on result objects
|
|
6310
|
+
MemberExpression(path) {
|
|
6311
|
+
if (t.isIdentifier(path.node.object) &&
|
|
6312
|
+
t.isIdentifier(path.node.property) &&
|
|
6313
|
+
path.node.property.name === 'length') {
|
|
6314
|
+
const objName = path.node.object.name;
|
|
6315
|
+
if (resultVariables.has(objName)) {
|
|
6316
|
+
const resultInfo = resultVariables.get(objName);
|
|
6317
|
+
violations.push({
|
|
6318
|
+
rule: 'validate-runview-runquery-result-access',
|
|
6319
|
+
severity: 'critical',
|
|
6320
|
+
line: path.node.loc?.start.line || 0,
|
|
6321
|
+
column: path.node.loc?.start.column || 0,
|
|
6322
|
+
message: `Cannot check .length on ${resultInfo.method} result directly. ${resultInfo.method} returns an object with Success and Results properties.
|
|
6323
|
+
Correct pattern:
|
|
6324
|
+
if (${resultInfo.varName}?.Success && ${resultInfo.varName}?.Results?.length > 0) {
|
|
6325
|
+
// Process ${resultInfo.varName}.Results array
|
|
6326
|
+
}`,
|
|
6327
|
+
code: `${objName}.length`
|
|
6328
|
+
});
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
},
|
|
6332
|
+
// Check for incorrect conditional checks
|
|
6333
|
+
IfStatement(path) {
|
|
6334
|
+
const test = path.node.test;
|
|
6335
|
+
// Pattern: if (result) or if (result.length)
|
|
6336
|
+
if (t.isIdentifier(test)) {
|
|
6337
|
+
const varName = test.name;
|
|
6338
|
+
if (resultVariables.has(varName)) {
|
|
6339
|
+
const resultInfo = resultVariables.get(varName);
|
|
6340
|
+
// Check if they're ONLY checking the result object without .Success
|
|
6341
|
+
let checksSuccess = false;
|
|
6342
|
+
let checksResults = false;
|
|
6343
|
+
// Scan the if block to see what they're doing with the result
|
|
6344
|
+
path.traverse({
|
|
6345
|
+
MemberExpression(innerPath) {
|
|
6346
|
+
if (t.isIdentifier(innerPath.node.object) &&
|
|
6347
|
+
innerPath.node.object.name === varName) {
|
|
6348
|
+
const prop = t.isIdentifier(innerPath.node.property) ? innerPath.node.property.name : '';
|
|
6349
|
+
if (prop === 'Success')
|
|
6350
|
+
checksSuccess = true;
|
|
6351
|
+
if (prop === 'Results')
|
|
6352
|
+
checksResults = true;
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
});
|
|
6356
|
+
// If they're accessing Results without checking Success, flag it
|
|
6357
|
+
if (checksResults && !checksSuccess) {
|
|
6358
|
+
violations.push({
|
|
6359
|
+
rule: 'validate-runview-runquery-result-access',
|
|
6360
|
+
severity: 'high',
|
|
6361
|
+
line: test.loc?.start.line || 0,
|
|
6362
|
+
column: test.loc?.start.column || 0,
|
|
6363
|
+
message: `Checking ${resultInfo.method} result without verifying Success property.
|
|
6364
|
+
Correct pattern:
|
|
6365
|
+
if (${resultInfo.varName}?.Success) {
|
|
6366
|
+
const data = ${resultInfo.varName}.Results;
|
|
6367
|
+
// Process data
|
|
6368
|
+
} else {
|
|
6369
|
+
// Handle error: ${resultInfo.varName}.ErrorMessage
|
|
6370
|
+
}`,
|
|
6371
|
+
code: `if (${varName})`
|
|
6372
|
+
});
|
|
6373
|
+
}
|
|
6374
|
+
}
|
|
6375
|
+
}
|
|
6376
|
+
// Pattern: if (result?.length)
|
|
6377
|
+
if (t.isOptionalMemberExpression(test) &&
|
|
6378
|
+
t.isIdentifier(test.object) &&
|
|
6379
|
+
t.isIdentifier(test.property) &&
|
|
6380
|
+
test.property.name === 'length') {
|
|
6381
|
+
const varName = test.object.name;
|
|
6382
|
+
if (resultVariables.has(varName)) {
|
|
6383
|
+
const resultInfo = resultVariables.get(varName);
|
|
6384
|
+
violations.push({
|
|
6385
|
+
rule: 'validate-runview-runquery-result-access',
|
|
6386
|
+
severity: 'critical',
|
|
6387
|
+
line: test.loc?.start.line || 0,
|
|
6388
|
+
column: test.loc?.start.column || 0,
|
|
6389
|
+
message: `Incorrect check: "${varName}?.length" on ${resultInfo.method} result.
|
|
6390
|
+
Correct pattern:
|
|
6391
|
+
if (${resultInfo.varName}?.Success && ${resultInfo.varName}?.Results?.length > 0) {
|
|
6392
|
+
const processedData = processChartData(${resultInfo.varName}.Results);
|
|
6393
|
+
// Use processedData
|
|
6394
|
+
}`,
|
|
6395
|
+
code: `if (${varName}?.length)`
|
|
6396
|
+
});
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
},
|
|
6400
|
+
// Check for passing result directly to functions expecting arrays
|
|
6401
|
+
CallExpression(path) {
|
|
6402
|
+
const args = path.node.arguments;
|
|
6403
|
+
for (let i = 0; i < args.length; i++) {
|
|
6404
|
+
const arg = args[i];
|
|
6405
|
+
if (t.isIdentifier(arg) && resultVariables.has(arg.name)) {
|
|
6406
|
+
const resultInfo = resultVariables.get(arg.name);
|
|
6407
|
+
// Check if the function being called looks like it expects an array
|
|
6408
|
+
let funcName = '';
|
|
6409
|
+
if (t.isIdentifier(path.node.callee)) {
|
|
6410
|
+
funcName = path.node.callee.name;
|
|
6411
|
+
}
|
|
6412
|
+
else if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
|
|
6413
|
+
funcName = path.node.callee.property.name;
|
|
6414
|
+
}
|
|
6415
|
+
// Common functions that expect arrays
|
|
6416
|
+
const arrayExpectingFuncs = [
|
|
6417
|
+
'map', 'filter', 'forEach', 'reduce', 'sort', 'concat',
|
|
6418
|
+
'processChartData', 'processData', 'transformData',
|
|
6419
|
+
'setData', 'setItems', 'setResults', 'setRows'
|
|
6420
|
+
];
|
|
6421
|
+
if (arrayExpectingFuncs.some(f => funcName.toLowerCase().includes(f.toLowerCase()))) {
|
|
6422
|
+
violations.push({
|
|
6423
|
+
rule: 'validate-runview-runquery-result-access',
|
|
6424
|
+
severity: 'critical',
|
|
6425
|
+
line: arg.loc?.start.line || 0,
|
|
6426
|
+
column: arg.loc?.start.column || 0,
|
|
6427
|
+
message: `Passing ${resultInfo.method} result object directly to ${funcName}() which expects an array.
|
|
6428
|
+
Correct pattern:
|
|
6429
|
+
if (${resultInfo.varName}?.Success) {
|
|
6430
|
+
${funcName}(${resultInfo.varName}.Results);
|
|
6431
|
+
} else {
|
|
6432
|
+
console.error('${resultInfo.method} failed:', ${resultInfo.varName}?.ErrorMessage);
|
|
6433
|
+
${funcName}([]); // Provide empty array as fallback
|
|
6434
|
+
}`,
|
|
6435
|
+
code: `${funcName}(${arg.name})`
|
|
6436
|
+
});
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
});
|
|
6442
|
+
return violations;
|
|
6443
|
+
}
|
|
6444
|
+
},
|
|
5496
6445
|
{
|
|
5497
6446
|
name: 'dependency-prop-validation',
|
|
5498
6447
|
appliesTo: 'all',
|
|
@@ -6074,6 +7023,1044 @@ ComponentLinter.universalComponentRules = [
|
|
|
6074
7023
|
});
|
|
6075
7024
|
return violations;
|
|
6076
7025
|
}
|
|
7026
|
+
},
|
|
7027
|
+
{
|
|
7028
|
+
name: 'validate-component-references',
|
|
7029
|
+
appliesTo: 'all',
|
|
7030
|
+
test: (ast, componentName, componentSpec) => {
|
|
7031
|
+
const violations = [];
|
|
7032
|
+
// Skip if no spec or no dependencies
|
|
7033
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
7034
|
+
return violations;
|
|
7035
|
+
}
|
|
7036
|
+
// Build a set of available component names from dependencies
|
|
7037
|
+
const availableComponents = new Set();
|
|
7038
|
+
for (const dep of componentSpec.dependencies) {
|
|
7039
|
+
if (dep.location === 'embedded' && dep.name) {
|
|
7040
|
+
availableComponents.add(dep.name);
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
// If no embedded dependencies, nothing to validate
|
|
7044
|
+
if (availableComponents.size === 0) {
|
|
7045
|
+
return violations;
|
|
7046
|
+
}
|
|
7047
|
+
// Track ALL defined variables in scope (from destructuring, imports, declarations, etc.)
|
|
7048
|
+
const definedVariables = new Set();
|
|
7049
|
+
const referencedComponents = new Set();
|
|
7050
|
+
// First pass: collect all variable declarations and destructuring
|
|
7051
|
+
(0, traverse_1.default)(ast, {
|
|
7052
|
+
// Track variable declarations (const x = ...)
|
|
7053
|
+
VariableDeclarator(path) {
|
|
7054
|
+
if (t.isIdentifier(path.node.id)) {
|
|
7055
|
+
definedVariables.add(path.node.id.name);
|
|
7056
|
+
}
|
|
7057
|
+
else if (t.isObjectPattern(path.node.id)) {
|
|
7058
|
+
// Track all destructured variables
|
|
7059
|
+
const collectDestructured = (pattern) => {
|
|
7060
|
+
for (const prop of pattern.properties) {
|
|
7061
|
+
if (t.isObjectProperty(prop)) {
|
|
7062
|
+
if (t.isIdentifier(prop.value)) {
|
|
7063
|
+
definedVariables.add(prop.value.name);
|
|
7064
|
+
}
|
|
7065
|
+
else if (t.isObjectPattern(prop.value)) {
|
|
7066
|
+
collectDestructured(prop.value);
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
7069
|
+
else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
7070
|
+
definedVariables.add(prop.argument.name);
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
7073
|
+
};
|
|
7074
|
+
collectDestructured(path.node.id);
|
|
7075
|
+
}
|
|
7076
|
+
else if (t.isArrayPattern(path.node.id)) {
|
|
7077
|
+
// Track array destructuring
|
|
7078
|
+
for (const elem of path.node.id.elements) {
|
|
7079
|
+
if (t.isIdentifier(elem)) {
|
|
7080
|
+
definedVariables.add(elem.name);
|
|
7081
|
+
}
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
},
|
|
7085
|
+
// Track function declarations
|
|
7086
|
+
FunctionDeclaration(path) {
|
|
7087
|
+
if (path.node.id) {
|
|
7088
|
+
definedVariables.add(path.node.id.name);
|
|
7089
|
+
}
|
|
7090
|
+
},
|
|
7091
|
+
// Track class declarations
|
|
7092
|
+
ClassDeclaration(path) {
|
|
7093
|
+
if (path.node.id) {
|
|
7094
|
+
definedVariables.add(path.node.id.name);
|
|
7095
|
+
}
|
|
7096
|
+
},
|
|
7097
|
+
// Track function parameters
|
|
7098
|
+
Function(path) {
|
|
7099
|
+
for (const param of path.node.params) {
|
|
7100
|
+
if (t.isIdentifier(param)) {
|
|
7101
|
+
definedVariables.add(param.name);
|
|
7102
|
+
}
|
|
7103
|
+
else if (t.isObjectPattern(param)) {
|
|
7104
|
+
// Track destructured parameters
|
|
7105
|
+
const collectParams = (pattern) => {
|
|
7106
|
+
for (const prop of pattern.properties) {
|
|
7107
|
+
if (t.isObjectProperty(prop)) {
|
|
7108
|
+
if (t.isIdentifier(prop.value)) {
|
|
7109
|
+
definedVariables.add(prop.value.name);
|
|
7110
|
+
}
|
|
7111
|
+
else if (t.isObjectPattern(prop.value)) {
|
|
7112
|
+
collectParams(prop.value);
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
};
|
|
7117
|
+
collectParams(param);
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
});
|
|
7122
|
+
// Second pass: check component usage
|
|
7123
|
+
(0, traverse_1.default)(ast, {
|
|
7124
|
+
// Look for React.createElement calls
|
|
7125
|
+
CallExpression(path) {
|
|
7126
|
+
const callee = path.node.callee;
|
|
7127
|
+
// Check for React.createElement(ComponentName, ...)
|
|
7128
|
+
if (t.isMemberExpression(callee) &&
|
|
7129
|
+
t.isIdentifier(callee.object) &&
|
|
7130
|
+
callee.object.name === 'React' &&
|
|
7131
|
+
t.isIdentifier(callee.property) &&
|
|
7132
|
+
callee.property.name === 'createElement') {
|
|
7133
|
+
const firstArg = path.node.arguments[0];
|
|
7134
|
+
// If first argument is an identifier (component reference)
|
|
7135
|
+
if (t.isIdentifier(firstArg)) {
|
|
7136
|
+
const componentRef = firstArg.name;
|
|
7137
|
+
// Skip HTML elements and React built-ins
|
|
7138
|
+
if (!componentRef.match(/^[a-z]/) && componentRef !== 'Fragment') {
|
|
7139
|
+
// Only check if it's supposed to be a component dependency
|
|
7140
|
+
// and it's not defined elsewhere in the code
|
|
7141
|
+
if (availableComponents.has(componentRef)) {
|
|
7142
|
+
referencedComponents.add(componentRef);
|
|
7143
|
+
}
|
|
7144
|
+
else if (!definedVariables.has(componentRef)) {
|
|
7145
|
+
// Only complain if it's not defined anywhere
|
|
7146
|
+
const availableList = Array.from(availableComponents).sort().join(', ');
|
|
7147
|
+
const availableLibs = componentSpec?.libraries?.map(lib => lib.globalVariable).filter(Boolean).join(', ') || '';
|
|
7148
|
+
let message = `Component "${componentRef}" is not defined. Available component dependencies: ${availableList}`;
|
|
7149
|
+
if (availableLibs) {
|
|
7150
|
+
message += `. Available libraries: ${availableLibs}`;
|
|
7151
|
+
}
|
|
7152
|
+
violations.push({
|
|
7153
|
+
rule: 'validate-component-references',
|
|
7154
|
+
severity: 'critical',
|
|
7155
|
+
line: firstArg.loc?.start.line || 0,
|
|
7156
|
+
column: firstArg.loc?.start.column || 0,
|
|
7157
|
+
message: message,
|
|
7158
|
+
code: `React.createElement(${componentRef}, ...)`
|
|
7159
|
+
});
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7164
|
+
},
|
|
7165
|
+
// Look for JSX elements
|
|
7166
|
+
JSXElement(path) {
|
|
7167
|
+
const openingElement = path.node.openingElement;
|
|
7168
|
+
const elementName = openingElement.name;
|
|
7169
|
+
if (t.isJSXIdentifier(elementName)) {
|
|
7170
|
+
const componentRef = elementName.name;
|
|
7171
|
+
// Skip HTML elements and fragments
|
|
7172
|
+
if (!componentRef.match(/^[a-z]/) && componentRef !== 'Fragment') {
|
|
7173
|
+
// Track if it's a known component dependency
|
|
7174
|
+
if (availableComponents.has(componentRef)) {
|
|
7175
|
+
referencedComponents.add(componentRef);
|
|
7176
|
+
}
|
|
7177
|
+
else if (!definedVariables.has(componentRef)) {
|
|
7178
|
+
// Only complain if it's not defined anywhere (not from libraries, not from declarations)
|
|
7179
|
+
const availableList = Array.from(availableComponents).sort().join(', ');
|
|
7180
|
+
const availableLibs = componentSpec?.libraries?.map(lib => lib.globalVariable).filter(Boolean).join(', ') || '';
|
|
7181
|
+
let message = `Component "${componentRef}" is not defined. Available component dependencies: ${availableList}`;
|
|
7182
|
+
if (availableLibs) {
|
|
7183
|
+
message += `. Available libraries: ${availableLibs}`;
|
|
7184
|
+
}
|
|
7185
|
+
violations.push({
|
|
7186
|
+
rule: 'validate-component-references',
|
|
7187
|
+
severity: 'critical',
|
|
7188
|
+
line: elementName.loc?.start.line || 0,
|
|
7189
|
+
column: elementName.loc?.start.column || 0,
|
|
7190
|
+
message: message,
|
|
7191
|
+
code: `<${componentRef} ... />`
|
|
7192
|
+
});
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
},
|
|
7197
|
+
// Look for destructuring from components prop specifically
|
|
7198
|
+
ObjectPattern(path) {
|
|
7199
|
+
// Check if this is destructuring from a 'components' parameter
|
|
7200
|
+
const parent = path.parent;
|
|
7201
|
+
// Check if it's a function parameter with components
|
|
7202
|
+
if ((t.isFunctionDeclaration(parent) || t.isFunctionExpression(parent) ||
|
|
7203
|
+
t.isArrowFunctionExpression(parent)) && parent.params.includes(path.node)) {
|
|
7204
|
+
// Look for components property
|
|
7205
|
+
for (const prop of path.node.properties) {
|
|
7206
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) &&
|
|
7207
|
+
prop.key.name === 'components' && t.isObjectPattern(prop.value)) {
|
|
7208
|
+
// Check each destructured component
|
|
7209
|
+
for (const componentProp of prop.value.properties) {
|
|
7210
|
+
if (t.isObjectProperty(componentProp) && t.isIdentifier(componentProp.key)) {
|
|
7211
|
+
const componentRef = componentProp.key.name;
|
|
7212
|
+
referencedComponents.add(componentRef);
|
|
7213
|
+
if (!availableComponents.has(componentRef)) {
|
|
7214
|
+
const availableList = Array.from(availableComponents).sort().join(', ');
|
|
7215
|
+
// Try to find similar names for suggestions
|
|
7216
|
+
const suggestions = Array.from(availableComponents).filter(name => name.toLowerCase().includes(componentRef.toLowerCase()) ||
|
|
7217
|
+
componentRef.toLowerCase().includes(name.toLowerCase()));
|
|
7218
|
+
let message = `Destructured component "${componentRef}" is not found in dependencies. Available components: ${availableList}`;
|
|
7219
|
+
if (suggestions.length > 0) {
|
|
7220
|
+
message += `. Did you mean: ${suggestions.join(' or ')}?`;
|
|
7221
|
+
}
|
|
7222
|
+
violations.push({
|
|
7223
|
+
rule: 'validate-component-references',
|
|
7224
|
+
severity: 'critical',
|
|
7225
|
+
line: componentProp.key.loc?.start.line || 0,
|
|
7226
|
+
column: componentProp.key.loc?.start.column || 0,
|
|
7227
|
+
message: message,
|
|
7228
|
+
code: `{ components: { ${componentRef}, ... } }`
|
|
7229
|
+
});
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
}
|
|
7237
|
+
});
|
|
7238
|
+
// Also warn about unused dependencies
|
|
7239
|
+
for (const depName of availableComponents) {
|
|
7240
|
+
if (!referencedComponents.has(depName)) {
|
|
7241
|
+
violations.push({
|
|
7242
|
+
rule: 'validate-component-references',
|
|
7243
|
+
severity: 'low',
|
|
7244
|
+
line: 1,
|
|
7245
|
+
column: 0,
|
|
7246
|
+
message: `Component dependency "${depName}" is defined but never used in the code.`,
|
|
7247
|
+
code: `dependencies: [..., { name: "${depName}", ... }, ...]`
|
|
7248
|
+
});
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7251
|
+
return violations;
|
|
7252
|
+
}
|
|
7253
|
+
},
|
|
7254
|
+
{
|
|
7255
|
+
name: 'unused-libraries',
|
|
7256
|
+
appliesTo: 'all',
|
|
7257
|
+
test: (ast, componentName, componentSpec) => {
|
|
7258
|
+
const violations = [];
|
|
7259
|
+
// Skip if no libraries declared
|
|
7260
|
+
if (!componentSpec?.libraries || componentSpec.libraries.length === 0) {
|
|
7261
|
+
return violations;
|
|
7262
|
+
}
|
|
7263
|
+
// Get the function body to search within
|
|
7264
|
+
let functionBody = '';
|
|
7265
|
+
(0, traverse_1.default)(ast, {
|
|
7266
|
+
FunctionDeclaration(path) {
|
|
7267
|
+
if (path.node.id && path.node.id.name === componentName) {
|
|
7268
|
+
functionBody = path.toString();
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
});
|
|
7272
|
+
// If we couldn't find the function body, use the whole code
|
|
7273
|
+
if (!functionBody) {
|
|
7274
|
+
functionBody = ast.toString ? ast.toString() : '';
|
|
7275
|
+
}
|
|
7276
|
+
// Track which libraries are used and unused
|
|
7277
|
+
const unusedLibraries = [];
|
|
7278
|
+
const usedLibraries = [];
|
|
7279
|
+
// Check each library for usage
|
|
7280
|
+
for (const lib of componentSpec.libraries) {
|
|
7281
|
+
const globalVar = lib.globalVariable;
|
|
7282
|
+
if (!globalVar)
|
|
7283
|
+
continue;
|
|
7284
|
+
// Check for various usage patterns
|
|
7285
|
+
const usagePatterns = [
|
|
7286
|
+
globalVar + '.', // Direct property access: Chart.defaults
|
|
7287
|
+
globalVar + '(', // Direct call: dayjs()
|
|
7288
|
+
'new ' + globalVar + '(', // Constructor: new Chart()
|
|
7289
|
+
globalVar + '[', // Array/property access: XLSX['utils']
|
|
7290
|
+
'= ' + globalVar, // Assignment: const myChart = Chart
|
|
7291
|
+
', ' + globalVar, // In parameter list
|
|
7292
|
+
'(' + globalVar, // Start of expression
|
|
7293
|
+
'{' + globalVar, // In object literal
|
|
7294
|
+
'<' + globalVar, // JSX component
|
|
7295
|
+
globalVar + ' ', // Followed by space (various uses)
|
|
7296
|
+
];
|
|
7297
|
+
const isUsed = usagePatterns.some(pattern => functionBody.includes(pattern));
|
|
7298
|
+
if (isUsed) {
|
|
7299
|
+
usedLibraries.push({ name: lib.name, globalVariable: globalVar });
|
|
7300
|
+
}
|
|
7301
|
+
else {
|
|
7302
|
+
unusedLibraries.push({ name: lib.name, globalVariable: globalVar });
|
|
7303
|
+
}
|
|
7304
|
+
}
|
|
7305
|
+
// Determine severity based on usage patterns
|
|
7306
|
+
const totalLibraries = componentSpec.libraries.length;
|
|
7307
|
+
const usedCount = usedLibraries.length;
|
|
7308
|
+
if (usedCount === 0 && totalLibraries > 0) {
|
|
7309
|
+
// CRITICAL: No libraries used at all
|
|
7310
|
+
violations.push({
|
|
7311
|
+
rule: 'unused-libraries',
|
|
7312
|
+
severity: 'critical',
|
|
7313
|
+
line: 1,
|
|
7314
|
+
column: 0,
|
|
7315
|
+
message: `CRITICAL: None of the ${totalLibraries} declared libraries are used. This indicates missing core functionality.`,
|
|
7316
|
+
code: `Unused libraries: ${unusedLibraries.map(l => l.name).join(', ')}`
|
|
7317
|
+
});
|
|
7318
|
+
}
|
|
7319
|
+
else if (unusedLibraries.length > 0) {
|
|
7320
|
+
// Some libraries unused, severity depends on ratio
|
|
7321
|
+
for (const lib of unusedLibraries) {
|
|
7322
|
+
const severity = totalLibraries === 1 ? 'high' : 'low';
|
|
7323
|
+
const contextMessage = totalLibraries === 1
|
|
7324
|
+
? 'This is the only declared library and it\'s not being used.'
|
|
7325
|
+
: `${usedCount} of ${totalLibraries} libraries are being used. This might be an alternative/optional library.`;
|
|
7326
|
+
violations.push({
|
|
7327
|
+
rule: 'unused-libraries',
|
|
7328
|
+
severity: severity,
|
|
7329
|
+
line: 1,
|
|
7330
|
+
column: 0,
|
|
7331
|
+
message: `Library "${lib.name}" (${lib.globalVariable}) is declared but not used. ${contextMessage}`,
|
|
7332
|
+
code: `Consider removing if not needed: { name: "${lib.name}", globalVariable: "${lib.globalVariable}" }`
|
|
7333
|
+
});
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
return violations;
|
|
7337
|
+
}
|
|
7338
|
+
},
|
|
7339
|
+
{
|
|
7340
|
+
name: 'unused-component-dependencies',
|
|
7341
|
+
appliesTo: 'all',
|
|
7342
|
+
test: (ast, componentName, componentSpec) => {
|
|
7343
|
+
const violations = [];
|
|
7344
|
+
// Skip if no dependencies declared
|
|
7345
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
7346
|
+
return violations;
|
|
7347
|
+
}
|
|
7348
|
+
// Filter to only embedded components
|
|
7349
|
+
const embeddedDeps = componentSpec.dependencies.filter(dep => dep.location === 'embedded' && dep.name);
|
|
7350
|
+
if (embeddedDeps.length === 0) {
|
|
7351
|
+
return violations;
|
|
7352
|
+
}
|
|
7353
|
+
// Get the function body to search within
|
|
7354
|
+
let functionBody = '';
|
|
7355
|
+
(0, traverse_1.default)(ast, {
|
|
7356
|
+
FunctionDeclaration(path) {
|
|
7357
|
+
if (path.node.id && path.node.id.name === componentName) {
|
|
7358
|
+
functionBody = path.toString();
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
});
|
|
7362
|
+
// If we couldn't find the function body, use the whole code
|
|
7363
|
+
if (!functionBody) {
|
|
7364
|
+
functionBody = ast.toString ? ast.toString() : '';
|
|
7365
|
+
}
|
|
7366
|
+
// Check each component dependency for usage
|
|
7367
|
+
for (const dep of embeddedDeps) {
|
|
7368
|
+
const depName = dep.name;
|
|
7369
|
+
// Check for various usage patterns
|
|
7370
|
+
// Components can be used directly (if destructured) or via components object
|
|
7371
|
+
const usagePatterns = [
|
|
7372
|
+
// Direct usage (after destructuring)
|
|
7373
|
+
'<' + depName + ' ', // JSX: <AccountList />
|
|
7374
|
+
'<' + depName + '>', // JSX: <AccountList>
|
|
7375
|
+
'<' + depName + '/', // JSX self-closing: <AccountList/>
|
|
7376
|
+
depName + '(', // Direct call: AccountList()
|
|
7377
|
+
'= ' + depName, // Assignment: const List = AccountList
|
|
7378
|
+
depName + ' ||', // Fallback: AccountList || DefaultComponent
|
|
7379
|
+
depName + ' &&', // Conditional: AccountList && ...
|
|
7380
|
+
depName + ' ?', // Ternary: AccountList ? ... : ...
|
|
7381
|
+
', ' + depName, // In parameter/array list
|
|
7382
|
+
'(' + depName, // Start of expression
|
|
7383
|
+
'{' + depName, // In object literal
|
|
7384
|
+
// Via components object
|
|
7385
|
+
'components.' + depName, // Dot notation: components.AccountList
|
|
7386
|
+
"components['" + depName + "']", // Bracket notation single quotes
|
|
7387
|
+
'components["' + depName + '"]', // Bracket notation double quotes
|
|
7388
|
+
'components[`' + depName + '`]', // Bracket notation template literal
|
|
7389
|
+
'<components.' + depName, // JSX via components: <components.AccountList
|
|
7390
|
+
];
|
|
7391
|
+
const isUsed = usagePatterns.some(pattern => functionBody.includes(pattern));
|
|
7392
|
+
if (!isUsed) {
|
|
7393
|
+
violations.push({
|
|
7394
|
+
rule: 'unused-component-dependencies',
|
|
7395
|
+
severity: 'low',
|
|
7396
|
+
line: 1,
|
|
7397
|
+
column: 0,
|
|
7398
|
+
message: `Component dependency "${depName}" is declared but never used. Consider removing it if not needed.`,
|
|
7399
|
+
code: `Expected usage: <${depName} /> or <components.${depName} />`
|
|
7400
|
+
});
|
|
7401
|
+
}
|
|
7402
|
+
}
|
|
7403
|
+
return violations;
|
|
7404
|
+
}
|
|
7405
|
+
},
|
|
7406
|
+
{
|
|
7407
|
+
name: 'component-usage-without-destructuring',
|
|
7408
|
+
appliesTo: 'all',
|
|
7409
|
+
test: (ast, componentName, componentSpec) => {
|
|
7410
|
+
const violations = [];
|
|
7411
|
+
// Skip if no dependencies
|
|
7412
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
7413
|
+
return violations;
|
|
7414
|
+
}
|
|
7415
|
+
// Track dependency names
|
|
7416
|
+
const dependencyNames = new Set(componentSpec.dependencies.map(d => d.name).filter(Boolean));
|
|
7417
|
+
// Track what's been destructured from components prop
|
|
7418
|
+
const destructuredComponents = new Set();
|
|
7419
|
+
(0, traverse_1.default)(ast, {
|
|
7420
|
+
// Track destructuring from components
|
|
7421
|
+
VariableDeclarator(path) {
|
|
7422
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
7423
|
+
// Check if destructuring from 'components'
|
|
7424
|
+
if (path.node.init.name === 'components') {
|
|
7425
|
+
for (const prop of path.node.id.properties) {
|
|
7426
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
7427
|
+
const name = prop.key.name;
|
|
7428
|
+
if (dependencyNames.has(name)) {
|
|
7429
|
+
destructuredComponents.add(name);
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
},
|
|
7436
|
+
// Also check function parameter destructuring
|
|
7437
|
+
FunctionDeclaration(path) {
|
|
7438
|
+
if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
|
|
7439
|
+
const param = path.node.params[0];
|
|
7440
|
+
if (t.isObjectPattern(param)) {
|
|
7441
|
+
for (const prop of param.properties) {
|
|
7442
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'components') {
|
|
7443
|
+
// Check for nested destructuring like { components: { A, B } }
|
|
7444
|
+
if (t.isObjectPattern(prop.value)) {
|
|
7445
|
+
for (const innerProp of prop.value.properties) {
|
|
7446
|
+
if (t.isObjectProperty(innerProp) && t.isIdentifier(innerProp.key)) {
|
|
7447
|
+
const name = innerProp.key.name;
|
|
7448
|
+
if (dependencyNames.has(name)) {
|
|
7449
|
+
destructuredComponents.add(name);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7458
|
+
},
|
|
7459
|
+
// Check JSX usage
|
|
7460
|
+
JSXElement(path) {
|
|
7461
|
+
const openingElement = path.node.openingElement;
|
|
7462
|
+
// Check for direct component usage (e.g., <ComponentName>)
|
|
7463
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
7464
|
+
const name = openingElement.name.name;
|
|
7465
|
+
// Check if this is one of our dependencies being used directly
|
|
7466
|
+
if (dependencyNames.has(name) && !destructuredComponents.has(name)) {
|
|
7467
|
+
violations.push({
|
|
7468
|
+
rule: 'component-usage-without-destructuring',
|
|
7469
|
+
severity: 'critical',
|
|
7470
|
+
line: openingElement.loc?.start.line || 0,
|
|
7471
|
+
column: openingElement.loc?.start.column || 0,
|
|
7472
|
+
message: `Component "${name}" used without destructuring. Either destructure it from components prop (const { ${name} } = components;) or use <components.${name} />`,
|
|
7473
|
+
code: `<${name}>`
|
|
7474
|
+
});
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
7478
|
+
});
|
|
7479
|
+
return violations;
|
|
7480
|
+
}
|
|
7481
|
+
},
|
|
7482
|
+
{
|
|
7483
|
+
name: 'prefer-jsx-syntax',
|
|
7484
|
+
appliesTo: 'all',
|
|
7485
|
+
test: (ast, componentName) => {
|
|
7486
|
+
const violations = [];
|
|
7487
|
+
(0, traverse_1.default)(ast, {
|
|
7488
|
+
CallExpression(path) {
|
|
7489
|
+
const callee = path.node.callee;
|
|
7490
|
+
// Check for React.createElement
|
|
7491
|
+
if (t.isMemberExpression(callee) &&
|
|
7492
|
+
t.isIdentifier(callee.object) &&
|
|
7493
|
+
callee.object.name === 'React' &&
|
|
7494
|
+
t.isIdentifier(callee.property) &&
|
|
7495
|
+
callee.property.name === 'createElement') {
|
|
7496
|
+
violations.push({
|
|
7497
|
+
rule: 'prefer-jsx-syntax',
|
|
7498
|
+
severity: 'low',
|
|
7499
|
+
line: callee.loc?.start.line || 0,
|
|
7500
|
+
column: callee.loc?.start.column || 0,
|
|
7501
|
+
message: 'Prefer JSX syntax over React.createElement for better readability',
|
|
7502
|
+
code: 'React.createElement(...) → <ComponentName ... />'
|
|
7503
|
+
});
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
});
|
|
7507
|
+
return violations;
|
|
7508
|
+
}
|
|
7509
|
+
},
|
|
7510
|
+
{
|
|
7511
|
+
name: 'prefer-async-await',
|
|
7512
|
+
appliesTo: 'all',
|
|
7513
|
+
test: (ast, componentName) => {
|
|
7514
|
+
const violations = [];
|
|
7515
|
+
(0, traverse_1.default)(ast, {
|
|
7516
|
+
CallExpression(path) {
|
|
7517
|
+
const callee = path.node.callee;
|
|
7518
|
+
// Check for .then() chains
|
|
7519
|
+
if (t.isMemberExpression(callee) &&
|
|
7520
|
+
t.isIdentifier(callee.property) &&
|
|
7521
|
+
callee.property.name === 'then') {
|
|
7522
|
+
// Try to get the context of what's being chained
|
|
7523
|
+
let context = '';
|
|
7524
|
+
if (t.isMemberExpression(callee.object)) {
|
|
7525
|
+
context = ' Consider using async/await for cleaner code.';
|
|
7526
|
+
}
|
|
7527
|
+
violations.push({
|
|
7528
|
+
rule: 'prefer-async-await',
|
|
7529
|
+
severity: 'low',
|
|
7530
|
+
line: callee.property.loc?.start.line || 0,
|
|
7531
|
+
column: callee.property.loc?.start.column || 0,
|
|
7532
|
+
message: `Prefer async/await over .then() chains for better readability.${context}`,
|
|
7533
|
+
code: '.then(result => ...) → const result = await ...'
|
|
7534
|
+
});
|
|
7535
|
+
}
|
|
7536
|
+
}
|
|
7537
|
+
});
|
|
7538
|
+
return violations;
|
|
7539
|
+
}
|
|
7540
|
+
},
|
|
7541
|
+
{
|
|
7542
|
+
name: 'single-function-only',
|
|
7543
|
+
appliesTo: 'all',
|
|
7544
|
+
test: (ast, componentName) => {
|
|
7545
|
+
const violations = [];
|
|
7546
|
+
// Count all function declarations and expressions at the top level
|
|
7547
|
+
const functionDeclarations = [];
|
|
7548
|
+
const functionExpressions = [];
|
|
7549
|
+
(0, traverse_1.default)(ast, {
|
|
7550
|
+
FunctionDeclaration(path) {
|
|
7551
|
+
// Only check top-level functions (not nested inside other functions)
|
|
7552
|
+
const parent = path.getFunctionParent();
|
|
7553
|
+
if (!parent) {
|
|
7554
|
+
const funcName = path.node.id?.name || 'anonymous';
|
|
7555
|
+
functionDeclarations.push({
|
|
7556
|
+
name: funcName,
|
|
7557
|
+
line: path.node.loc?.start.line || 0,
|
|
7558
|
+
column: path.node.loc?.start.column || 0
|
|
7559
|
+
});
|
|
7560
|
+
}
|
|
7561
|
+
},
|
|
7562
|
+
VariableDeclaration(path) {
|
|
7563
|
+
// Check for const/let/var func = function() or arrow functions at top level
|
|
7564
|
+
const parent = path.getFunctionParent();
|
|
7565
|
+
if (!parent) {
|
|
7566
|
+
for (const declarator of path.node.declarations) {
|
|
7567
|
+
if (t.isVariableDeclarator(declarator) &&
|
|
7568
|
+
(t.isFunctionExpression(declarator.init) ||
|
|
7569
|
+
t.isArrowFunctionExpression(declarator.init))) {
|
|
7570
|
+
const funcName = t.isIdentifier(declarator.id) ? declarator.id.name : 'anonymous';
|
|
7571
|
+
functionExpressions.push({
|
|
7572
|
+
name: funcName,
|
|
7573
|
+
line: declarator.loc?.start.line || 0,
|
|
7574
|
+
column: declarator.loc?.start.column || 0
|
|
7575
|
+
});
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
}
|
|
7580
|
+
});
|
|
7581
|
+
const allFunctions = [...functionDeclarations, ...functionExpressions];
|
|
7582
|
+
// Check if we have more than one function
|
|
7583
|
+
if (allFunctions.length > 1) {
|
|
7584
|
+
// Find which one is the main component
|
|
7585
|
+
const mainComponentIndex = allFunctions.findIndex(f => f.name === componentName);
|
|
7586
|
+
const otherFunctions = allFunctions.filter((_, index) => index !== mainComponentIndex);
|
|
7587
|
+
violations.push({
|
|
7588
|
+
rule: 'single-function-only',
|
|
7589
|
+
severity: 'critical',
|
|
7590
|
+
line: otherFunctions[0].line,
|
|
7591
|
+
column: otherFunctions[0].column,
|
|
7592
|
+
message: `Component code must contain ONLY the main component function "${componentName}". Found ${allFunctions.length} functions: ${allFunctions.map(f => f.name).join(', ')}. Move other functions to separate component dependencies.`,
|
|
7593
|
+
code: `Remove functions: ${otherFunctions.map(f => f.name).join(', ')}`
|
|
7594
|
+
});
|
|
7595
|
+
// Add a violation for each extra function
|
|
7596
|
+
for (const func of otherFunctions) {
|
|
7597
|
+
violations.push({
|
|
7598
|
+
rule: 'single-function-only',
|
|
7599
|
+
severity: 'critical',
|
|
7600
|
+
line: func.line,
|
|
7601
|
+
column: func.column,
|
|
7602
|
+
message: `Extra function "${func.name}" not allowed. Each component must be a single function. Move this to a separate component dependency.`,
|
|
7603
|
+
code: `function ${func.name} should be a separate component`
|
|
7604
|
+
});
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
// Also check that the single function matches the component name
|
|
7608
|
+
if (allFunctions.length === 1 && allFunctions[0].name !== componentName) {
|
|
7609
|
+
violations.push({
|
|
7610
|
+
rule: 'single-function-only',
|
|
7611
|
+
severity: 'critical',
|
|
7612
|
+
line: allFunctions[0].line,
|
|
7613
|
+
column: allFunctions[0].column,
|
|
7614
|
+
message: `Component function name "${allFunctions[0].name}" does not match component name "${componentName}". The function must be named exactly as specified.`,
|
|
7615
|
+
code: `Rename function to: function ${componentName}(...)`
|
|
7616
|
+
});
|
|
7617
|
+
}
|
|
7618
|
+
// Check for no function at all
|
|
7619
|
+
if (allFunctions.length === 0) {
|
|
7620
|
+
violations.push({
|
|
7621
|
+
rule: 'single-function-only',
|
|
7622
|
+
severity: 'critical',
|
|
7623
|
+
line: 1,
|
|
7624
|
+
column: 0,
|
|
7625
|
+
message: `Component code must contain exactly one function named "${componentName}". No functions found.`,
|
|
7626
|
+
code: `Add: function ${componentName}({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) { ... }`
|
|
7627
|
+
});
|
|
7628
|
+
}
|
|
7629
|
+
return violations;
|
|
7630
|
+
}
|
|
7631
|
+
},
|
|
7632
|
+
// New rules for catching RunQuery/RunView result access patterns
|
|
7633
|
+
{
|
|
7634
|
+
name: 'runquery-runview-ternary-array-check',
|
|
7635
|
+
appliesTo: 'all',
|
|
7636
|
+
test: (ast, componentName, componentSpec) => {
|
|
7637
|
+
const violations = [];
|
|
7638
|
+
// Track variables that hold RunView/RunQuery results
|
|
7639
|
+
const resultVariables = new Map();
|
|
7640
|
+
// First pass: identify all RunView/RunQuery calls and their assigned variables
|
|
7641
|
+
(0, traverse_1.default)(ast, {
|
|
7642
|
+
AwaitExpression(path) {
|
|
7643
|
+
const callExpr = path.node.argument;
|
|
7644
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
7645
|
+
const callee = callExpr.callee;
|
|
7646
|
+
// Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
|
|
7647
|
+
if (t.isMemberExpression(callee.object) &&
|
|
7648
|
+
t.isIdentifier(callee.object.object) &&
|
|
7649
|
+
callee.object.object.name === 'utilities' &&
|
|
7650
|
+
t.isIdentifier(callee.object.property)) {
|
|
7651
|
+
const subObject = callee.object.property.name;
|
|
7652
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
7653
|
+
let methodType = null;
|
|
7654
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
7655
|
+
methodType = method;
|
|
7656
|
+
}
|
|
7657
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
7658
|
+
methodType = 'RunQuery';
|
|
7659
|
+
}
|
|
7660
|
+
if (methodType) {
|
|
7661
|
+
// Check if this is being assigned to a variable
|
|
7662
|
+
const parent = path.parent;
|
|
7663
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
7664
|
+
// const result = await utilities.rv.RunView(...)
|
|
7665
|
+
resultVariables.set(parent.id.name, {
|
|
7666
|
+
line: parent.id.loc?.start.line || 0,
|
|
7667
|
+
column: parent.id.loc?.start.column || 0,
|
|
7668
|
+
method: methodType,
|
|
7669
|
+
varName: parent.id.name
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7672
|
+
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
7673
|
+
// result = await utilities.rv.RunView(...)
|
|
7674
|
+
resultVariables.set(parent.left.name, {
|
|
7675
|
+
line: parent.left.loc?.start.line || 0,
|
|
7676
|
+
column: parent.left.loc?.start.column || 0,
|
|
7677
|
+
method: methodType,
|
|
7678
|
+
varName: parent.left.name
|
|
7679
|
+
});
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
});
|
|
7686
|
+
// Second pass: check for Array.isArray(result) ? result : [] pattern
|
|
7687
|
+
(0, traverse_1.default)(ast, {
|
|
7688
|
+
ConditionalExpression(path) {
|
|
7689
|
+
const test = path.node.test;
|
|
7690
|
+
const consequent = path.node.consequent;
|
|
7691
|
+
const alternate = path.node.alternate;
|
|
7692
|
+
// Check for Array.isArray(variable) pattern
|
|
7693
|
+
if (t.isCallExpression(test) &&
|
|
7694
|
+
t.isMemberExpression(test.callee) &&
|
|
7695
|
+
t.isIdentifier(test.callee.object) &&
|
|
7696
|
+
test.callee.object.name === 'Array' &&
|
|
7697
|
+
t.isIdentifier(test.callee.property) &&
|
|
7698
|
+
test.callee.property.name === 'isArray' &&
|
|
7699
|
+
test.arguments.length === 1 &&
|
|
7700
|
+
t.isIdentifier(test.arguments[0])) {
|
|
7701
|
+
const varName = test.arguments[0].name;
|
|
7702
|
+
// Check if this variable is a RunQuery/RunView result
|
|
7703
|
+
if (resultVariables.has(varName)) {
|
|
7704
|
+
const resultInfo = resultVariables.get(varName);
|
|
7705
|
+
// Check if the consequent is the same variable and alternate is []
|
|
7706
|
+
if (t.isIdentifier(consequent) &&
|
|
7707
|
+
consequent.name === varName &&
|
|
7708
|
+
t.isArrayExpression(alternate) &&
|
|
7709
|
+
alternate.elements.length === 0) {
|
|
7710
|
+
violations.push({
|
|
7711
|
+
rule: 'runquery-runview-ternary-array-check',
|
|
7712
|
+
severity: 'critical',
|
|
7713
|
+
line: test.loc?.start.line || 0,
|
|
7714
|
+
column: test.loc?.start.column || 0,
|
|
7715
|
+
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 }.
|
|
7716
|
+
|
|
7717
|
+
Correct patterns:
|
|
7718
|
+
// Option 1: Simple with fallback
|
|
7719
|
+
${varName}.Results || []
|
|
7720
|
+
|
|
7721
|
+
// Option 2: Check success first
|
|
7722
|
+
if (${varName}.Success) {
|
|
7723
|
+
setData(${varName}.Results || []);
|
|
7724
|
+
} else {
|
|
7725
|
+
console.error('Failed:', ${varName}.ErrorMessage);
|
|
7726
|
+
setData([]);
|
|
7727
|
+
}`,
|
|
7728
|
+
code: `Array.isArray(${varName}) ? ${varName} : []`
|
|
7729
|
+
});
|
|
7730
|
+
}
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
});
|
|
7735
|
+
return violations;
|
|
7736
|
+
}
|
|
7737
|
+
},
|
|
7738
|
+
{
|
|
7739
|
+
name: 'runquery-runview-direct-setstate',
|
|
7740
|
+
appliesTo: 'all',
|
|
7741
|
+
test: (ast, componentName, componentSpec) => {
|
|
7742
|
+
const violations = [];
|
|
7743
|
+
// Track variables that hold RunView/RunQuery results
|
|
7744
|
+
const resultVariables = new Map();
|
|
7745
|
+
// First pass: identify all RunView/RunQuery calls and their assigned variables
|
|
7746
|
+
(0, traverse_1.default)(ast, {
|
|
7747
|
+
AwaitExpression(path) {
|
|
7748
|
+
const callExpr = path.node.argument;
|
|
7749
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
7750
|
+
const callee = callExpr.callee;
|
|
7751
|
+
// Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
|
|
7752
|
+
if (t.isMemberExpression(callee.object) &&
|
|
7753
|
+
t.isIdentifier(callee.object.object) &&
|
|
7754
|
+
callee.object.object.name === 'utilities' &&
|
|
7755
|
+
t.isIdentifier(callee.object.property)) {
|
|
7756
|
+
const subObject = callee.object.property.name;
|
|
7757
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
7758
|
+
let methodType = null;
|
|
7759
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
7760
|
+
methodType = method;
|
|
7761
|
+
}
|
|
7762
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
7763
|
+
methodType = 'RunQuery';
|
|
7764
|
+
}
|
|
7765
|
+
if (methodType) {
|
|
7766
|
+
// Check if this is being assigned to a variable
|
|
7767
|
+
const parent = path.parent;
|
|
7768
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
7769
|
+
resultVariables.set(parent.id.name, {
|
|
7770
|
+
line: parent.id.loc?.start.line || 0,
|
|
7771
|
+
column: parent.id.loc?.start.column || 0,
|
|
7772
|
+
method: methodType,
|
|
7773
|
+
varName: parent.id.name
|
|
7774
|
+
});
|
|
7775
|
+
}
|
|
7776
|
+
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
7777
|
+
resultVariables.set(parent.left.name, {
|
|
7778
|
+
line: parent.left.loc?.start.line || 0,
|
|
7779
|
+
column: parent.left.loc?.start.column || 0,
|
|
7780
|
+
method: methodType,
|
|
7781
|
+
varName: parent.left.name
|
|
7782
|
+
});
|
|
7783
|
+
}
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
});
|
|
7789
|
+
// Second pass: check for passing result directly to setState functions
|
|
7790
|
+
(0, traverse_1.default)(ast, {
|
|
7791
|
+
CallExpression(path) {
|
|
7792
|
+
const callee = path.node.callee;
|
|
7793
|
+
// Check if this is a setState function call
|
|
7794
|
+
if (t.isIdentifier(callee)) {
|
|
7795
|
+
const funcName = callee.name;
|
|
7796
|
+
// Common setState patterns
|
|
7797
|
+
const setStatePatterns = [
|
|
7798
|
+
/^set[A-Z]/, // setData, setChartData, setItems, etc.
|
|
7799
|
+
/^update[A-Z]/, // updateData, updateItems, etc.
|
|
7800
|
+
];
|
|
7801
|
+
const isSetStateFunction = setStatePatterns.some(pattern => pattern.test(funcName));
|
|
7802
|
+
if (isSetStateFunction && path.node.arguments.length > 0) {
|
|
7803
|
+
const firstArg = path.node.arguments[0];
|
|
7804
|
+
// Check if the argument is a ternary with Array.isArray check
|
|
7805
|
+
if (t.isConditionalExpression(firstArg)) {
|
|
7806
|
+
const test = firstArg.test;
|
|
7807
|
+
const consequent = firstArg.consequent;
|
|
7808
|
+
const alternate = firstArg.alternate;
|
|
7809
|
+
// Check for Array.isArray(variable) ? variable : []
|
|
7810
|
+
if (t.isCallExpression(test) &&
|
|
7811
|
+
t.isMemberExpression(test.callee) &&
|
|
7812
|
+
t.isIdentifier(test.callee.object) &&
|
|
7813
|
+
test.callee.object.name === 'Array' &&
|
|
7814
|
+
t.isIdentifier(test.callee.property) &&
|
|
7815
|
+
test.callee.property.name === 'isArray' &&
|
|
7816
|
+
test.arguments.length === 1 &&
|
|
7817
|
+
t.isIdentifier(test.arguments[0])) {
|
|
7818
|
+
const varName = test.arguments[0].name;
|
|
7819
|
+
if (resultVariables.has(varName) &&
|
|
7820
|
+
t.isIdentifier(consequent) &&
|
|
7821
|
+
consequent.name === varName) {
|
|
7822
|
+
const resultInfo = resultVariables.get(varName);
|
|
7823
|
+
violations.push({
|
|
7824
|
+
rule: 'runquery-runview-direct-setstate',
|
|
7825
|
+
severity: 'critical',
|
|
7826
|
+
line: firstArg.loc?.start.line || 0,
|
|
7827
|
+
column: firstArg.loc?.start.column || 0,
|
|
7828
|
+
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.
|
|
7829
|
+
|
|
7830
|
+
Correct pattern:
|
|
7831
|
+
if (${varName}.Success) {
|
|
7832
|
+
${funcName}(${varName}.Results || []);
|
|
7833
|
+
} else {
|
|
7834
|
+
console.error('Failed to load data:', ${varName}.ErrorMessage);
|
|
7835
|
+
${funcName}([]);
|
|
7836
|
+
}
|
|
7837
|
+
|
|
7838
|
+
// Or simpler:
|
|
7839
|
+
${funcName}(${varName}.Results || []);`,
|
|
7840
|
+
code: `${funcName}(Array.isArray(${varName}) ? ${varName} : [])`
|
|
7841
|
+
});
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
7845
|
+
// Check if passing result directly (not accessing .Results)
|
|
7846
|
+
if (t.isIdentifier(firstArg) && resultVariables.has(firstArg.name)) {
|
|
7847
|
+
const resultInfo = resultVariables.get(firstArg.name);
|
|
7848
|
+
violations.push({
|
|
7849
|
+
rule: 'runquery-runview-direct-setstate',
|
|
7850
|
+
severity: 'critical',
|
|
7851
|
+
line: firstArg.loc?.start.line || 0,
|
|
7852
|
+
column: firstArg.loc?.start.column || 0,
|
|
7853
|
+
message: `Passing ${resultInfo.method} result object directly to ${funcName}. The result is an object { Success, Results, ErrorMessage }, not the data array.
|
|
7854
|
+
|
|
7855
|
+
Correct pattern:
|
|
7856
|
+
if (${firstArg.name}.Success) {
|
|
7857
|
+
${funcName}(${firstArg.name}.Results || []);
|
|
7858
|
+
} else {
|
|
7859
|
+
console.error('Failed to load data:', ${firstArg.name}.ErrorMessage);
|
|
7860
|
+
${funcName}([]);
|
|
7861
|
+
}`,
|
|
7862
|
+
code: `${funcName}(${firstArg.name})`
|
|
7863
|
+
});
|
|
7864
|
+
}
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
});
|
|
7869
|
+
return violations;
|
|
7870
|
+
}
|
|
7871
|
+
},
|
|
7872
|
+
{
|
|
7873
|
+
name: 'styles-invalid-path',
|
|
7874
|
+
appliesTo: 'all',
|
|
7875
|
+
test: (ast, componentName, componentSpec) => {
|
|
7876
|
+
const violations = [];
|
|
7877
|
+
const analyzer = ComponentLinter.getStylesAnalyzer();
|
|
7878
|
+
(0, traverse_1.default)(ast, {
|
|
7879
|
+
MemberExpression(path) {
|
|
7880
|
+
// Build the complete property chain first
|
|
7881
|
+
let propertyChain = [];
|
|
7882
|
+
let current = path.node;
|
|
7883
|
+
// Walk up from the deepest member expression to build the full chain
|
|
7884
|
+
while (t.isMemberExpression(current)) {
|
|
7885
|
+
if (t.isIdentifier(current.property)) {
|
|
7886
|
+
propertyChain.unshift(current.property.name);
|
|
7887
|
+
}
|
|
7888
|
+
if (t.isIdentifier(current.object)) {
|
|
7889
|
+
propertyChain.unshift(current.object.name);
|
|
7890
|
+
break;
|
|
7891
|
+
}
|
|
7892
|
+
current = current.object;
|
|
7893
|
+
}
|
|
7894
|
+
// Only process if this is a styles access
|
|
7895
|
+
if (propertyChain[0] === 'styles') {
|
|
7896
|
+
// Validate the path
|
|
7897
|
+
if (!analyzer.isValidPath(propertyChain)) {
|
|
7898
|
+
const suggestions = analyzer.getSuggestionsForPath(propertyChain);
|
|
7899
|
+
const accessPath = propertyChain.join('.');
|
|
7900
|
+
let message = `Invalid styles property path: "${accessPath}"`;
|
|
7901
|
+
if (suggestions.didYouMean) {
|
|
7902
|
+
message += `\n\nDid you mean: ${suggestions.didYouMean}?`;
|
|
7903
|
+
}
|
|
7904
|
+
if (suggestions.correctPaths.length > 0) {
|
|
7905
|
+
message += `\n\nThe property "${propertyChain[propertyChain.length - 1]}" exists at:`;
|
|
7906
|
+
suggestions.correctPaths.forEach((p) => {
|
|
7907
|
+
message += `\n - ${p}`;
|
|
7908
|
+
});
|
|
7909
|
+
}
|
|
7910
|
+
if (suggestions.availableAtParent.length > 0) {
|
|
7911
|
+
const parentPath = propertyChain.slice(0, -1).join('.');
|
|
7912
|
+
message += `\n\nAvailable properties at ${parentPath}:`;
|
|
7913
|
+
message += `\n ${suggestions.availableAtParent.slice(0, 5).join(', ')}`;
|
|
7914
|
+
if (suggestions.availableAtParent.length > 5) {
|
|
7915
|
+
message += ` (and ${suggestions.availableAtParent.length - 5} more)`;
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
// Get a contextual default value
|
|
7919
|
+
const defaultValue = analyzer.getDefaultValueForPath(propertyChain);
|
|
7920
|
+
message += `\n\nSuggested fix with safe access:\n ${accessPath.replace(/\./g, '?.')} || ${defaultValue}`;
|
|
7921
|
+
violations.push({
|
|
7922
|
+
rule: 'styles-invalid-path',
|
|
7923
|
+
severity: 'critical',
|
|
7924
|
+
line: path.node.loc?.start.line || 0,
|
|
7925
|
+
column: path.node.loc?.start.column || 0,
|
|
7926
|
+
message: message,
|
|
7927
|
+
code: accessPath
|
|
7928
|
+
});
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
});
|
|
7933
|
+
return violations;
|
|
7934
|
+
}
|
|
7935
|
+
},
|
|
7936
|
+
{
|
|
7937
|
+
name: 'styles-unsafe-access',
|
|
7938
|
+
appliesTo: 'all',
|
|
7939
|
+
test: (ast, componentName, componentSpec) => {
|
|
7940
|
+
const violations = [];
|
|
7941
|
+
const analyzer = ComponentLinter.getStylesAnalyzer();
|
|
7942
|
+
(0, traverse_1.default)(ast, {
|
|
7943
|
+
MemberExpression(path) {
|
|
7944
|
+
// Build the complete property chain first
|
|
7945
|
+
let propertyChain = [];
|
|
7946
|
+
let current = path.node;
|
|
7947
|
+
let hasOptionalChaining = path.node.optional || false;
|
|
7948
|
+
// Walk up from the deepest member expression to build the full chain
|
|
7949
|
+
while (t.isMemberExpression(current)) {
|
|
7950
|
+
if (current.optional) {
|
|
7951
|
+
hasOptionalChaining = true;
|
|
7952
|
+
}
|
|
7953
|
+
if (t.isIdentifier(current.property)) {
|
|
7954
|
+
propertyChain.unshift(current.property.name);
|
|
7955
|
+
}
|
|
7956
|
+
if (t.isIdentifier(current.object)) {
|
|
7957
|
+
propertyChain.unshift(current.object.name);
|
|
7958
|
+
break;
|
|
7959
|
+
}
|
|
7960
|
+
current = current.object;
|
|
7961
|
+
}
|
|
7962
|
+
// Only process if this is a styles access
|
|
7963
|
+
if (propertyChain[0] === 'styles') {
|
|
7964
|
+
// Only check valid paths for safe access
|
|
7965
|
+
if (analyzer.isValidPath(propertyChain)) {
|
|
7966
|
+
// Check if this is a nested access without optional chaining or fallback
|
|
7967
|
+
if (propertyChain.length > 2 && !hasOptionalChaining) {
|
|
7968
|
+
// Check if there's a fallback (|| operator)
|
|
7969
|
+
const parent = path.parent;
|
|
7970
|
+
const hasFallback = t.isLogicalExpression(parent) && parent.operator === '||';
|
|
7971
|
+
if (!hasFallback) {
|
|
7972
|
+
const accessPath = propertyChain.join('.');
|
|
7973
|
+
const defaultValue = analyzer.getDefaultValueForPath(propertyChain);
|
|
7974
|
+
violations.push({
|
|
7975
|
+
rule: 'styles-unsafe-access',
|
|
7976
|
+
severity: 'high',
|
|
7977
|
+
line: path.node.loc?.start.line || 0,
|
|
7978
|
+
column: path.node.loc?.start.column || 0,
|
|
7979
|
+
message: `Unsafe styles property access: "${accessPath}". While this path is valid, you should use optional chaining for safety.
|
|
7980
|
+
|
|
7981
|
+
Example with optional chaining:
|
|
7982
|
+
${accessPath.replace(/\./g, '?.')} || ${defaultValue}
|
|
7983
|
+
|
|
7984
|
+
This prevents runtime errors if the styles object structure changes.`,
|
|
7985
|
+
code: accessPath
|
|
7986
|
+
});
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
});
|
|
7993
|
+
return violations;
|
|
7994
|
+
}
|
|
7995
|
+
},
|
|
7996
|
+
{
|
|
7997
|
+
name: 'runquery-runview-spread-operator',
|
|
7998
|
+
appliesTo: 'all',
|
|
7999
|
+
test: (ast, componentName, componentSpec) => {
|
|
8000
|
+
const violations = [];
|
|
8001
|
+
// Track variables that hold RunView/RunQuery results
|
|
8002
|
+
const resultVariables = new Map();
|
|
8003
|
+
// First pass: identify all RunView/RunQuery calls
|
|
8004
|
+
(0, traverse_1.default)(ast, {
|
|
8005
|
+
AwaitExpression(path) {
|
|
8006
|
+
const callExpr = path.node.argument;
|
|
8007
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
8008
|
+
const callee = callExpr.callee;
|
|
8009
|
+
if (t.isMemberExpression(callee.object) &&
|
|
8010
|
+
t.isIdentifier(callee.object.object) &&
|
|
8011
|
+
callee.object.object.name === 'utilities' &&
|
|
8012
|
+
t.isIdentifier(callee.object.property)) {
|
|
8013
|
+
const subObject = callee.object.property.name;
|
|
8014
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
8015
|
+
let methodType = null;
|
|
8016
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
8017
|
+
methodType = method;
|
|
8018
|
+
}
|
|
8019
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
8020
|
+
methodType = 'RunQuery';
|
|
8021
|
+
}
|
|
8022
|
+
if (methodType) {
|
|
8023
|
+
const parent = path.parent;
|
|
8024
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
8025
|
+
resultVariables.set(parent.id.name, {
|
|
8026
|
+
line: parent.id.loc?.start.line || 0,
|
|
8027
|
+
column: parent.id.loc?.start.column || 0,
|
|
8028
|
+
method: methodType,
|
|
8029
|
+
varName: parent.id.name
|
|
8030
|
+
});
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
});
|
|
8037
|
+
// Second pass: check for spread operator usage
|
|
8038
|
+
(0, traverse_1.default)(ast, {
|
|
8039
|
+
SpreadElement(path) {
|
|
8040
|
+
if (t.isIdentifier(path.node.argument)) {
|
|
8041
|
+
const varName = path.node.argument.name;
|
|
8042
|
+
if (resultVariables.has(varName)) {
|
|
8043
|
+
const resultInfo = resultVariables.get(varName);
|
|
8044
|
+
violations.push({
|
|
8045
|
+
rule: 'runquery-runview-spread-operator',
|
|
8046
|
+
severity: 'critical',
|
|
8047
|
+
line: path.node.loc?.start.line || 0,
|
|
8048
|
+
column: path.node.loc?.start.column || 0,
|
|
8049
|
+
message: `Cannot use spread operator on ${resultInfo.method} result object. Use ...${varName}.Results to spread the data array.
|
|
8050
|
+
|
|
8051
|
+
Correct pattern:
|
|
8052
|
+
const allData = [...existingData, ...${varName}.Results];
|
|
8053
|
+
|
|
8054
|
+
// Or with null safety:
|
|
8055
|
+
const allData = [...existingData, ...(${varName}.Results || [])];`,
|
|
8056
|
+
code: `...${varName}`
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
});
|
|
8062
|
+
return violations;
|
|
8063
|
+
}
|
|
6077
8064
|
}
|
|
6078
8065
|
];
|
|
6079
8066
|
//# sourceMappingURL=component-linter.js.map
|