@memberjunction/react-test-harness 2.94.0 → 2.95.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 +5 -0
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +1225 -160
- 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 +44 -49
- package/dist/lib/component-runner.js.map +1 -1
- package/package.json +3 -3
|
@@ -91,7 +91,8 @@ function getLineNumber(code, index) {
|
|
|
91
91
|
// These will be evaluated at TypeScript compile time and become static arrays
|
|
92
92
|
const runQueryResultProps = [
|
|
93
93
|
'QueryID', 'QueryName', 'Success', 'Results', 'RowCount',
|
|
94
|
-
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
94
|
+
'TotalRowCount', 'ExecutionTime', 'ErrorMessage', 'AppliedParameters',
|
|
95
|
+
'CacheHit', 'CacheKey', 'CacheTTLRemaining'
|
|
95
96
|
];
|
|
96
97
|
const runViewResultProps = [
|
|
97
98
|
'Success', 'Results', 'UserViewRunID', 'RowCount',
|
|
@@ -152,13 +153,78 @@ class ComponentLinter {
|
|
|
152
153
|
}
|
|
153
154
|
return isFromMethod;
|
|
154
155
|
}
|
|
156
|
+
static async validateComponentSyntax(code, componentName) {
|
|
157
|
+
try {
|
|
158
|
+
const parseResult = parser.parse(code, {
|
|
159
|
+
sourceType: 'module',
|
|
160
|
+
plugins: ['jsx', 'typescript'],
|
|
161
|
+
errorRecovery: true,
|
|
162
|
+
ranges: true
|
|
163
|
+
});
|
|
164
|
+
if (parseResult.errors && parseResult.errors.length > 0) {
|
|
165
|
+
const errors = parseResult.errors.map((error) => {
|
|
166
|
+
const location = error.loc ? `Line ${error.loc.line}, Column ${error.loc.column}` : 'Unknown location';
|
|
167
|
+
return `${location}: ${error.message || error.toString()}`;
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
valid: false,
|
|
171
|
+
errors
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
valid: true,
|
|
176
|
+
errors: []
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
// Handle catastrophic parse failures
|
|
181
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
|
|
182
|
+
return {
|
|
183
|
+
valid: false,
|
|
184
|
+
errors: [`Failed to parse component: ${errorMessage}`]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
155
188
|
static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode, options) {
|
|
156
189
|
try {
|
|
157
|
-
|
|
190
|
+
// Parse with error recovery to get both AST and errors
|
|
191
|
+
const parseResult = parser.parse(code, {
|
|
158
192
|
sourceType: 'module',
|
|
159
193
|
plugins: ['jsx', 'typescript'],
|
|
160
|
-
errorRecovery: true
|
|
194
|
+
errorRecovery: true,
|
|
195
|
+
attachComment: false,
|
|
196
|
+
ranges: true,
|
|
197
|
+
tokens: false
|
|
161
198
|
});
|
|
199
|
+
// Check for syntax errors from parser
|
|
200
|
+
const syntaxViolations = [];
|
|
201
|
+
if (parseResult.errors && parseResult.errors.length > 0) {
|
|
202
|
+
for (const error of parseResult.errors) {
|
|
203
|
+
const err = error; // Babel parser errors don't have proper types
|
|
204
|
+
syntaxViolations.push({
|
|
205
|
+
rule: 'syntax-error',
|
|
206
|
+
severity: 'critical',
|
|
207
|
+
line: err.loc?.line || 0,
|
|
208
|
+
column: err.loc?.column || 0,
|
|
209
|
+
message: `Syntax error in component "${componentName}": ${err.message || err.toString()}`,
|
|
210
|
+
code: err.code || 'BABEL_PARSER_ERROR'
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// If we have critical syntax errors, return immediately with those
|
|
215
|
+
if (syntaxViolations.length > 0) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
violations: syntaxViolations,
|
|
219
|
+
suggestions: this.generateSyntaxErrorSuggestions(syntaxViolations),
|
|
220
|
+
criticalCount: syntaxViolations.length,
|
|
221
|
+
highCount: 0,
|
|
222
|
+
mediumCount: 0,
|
|
223
|
+
lowCount: 0
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// Continue with existing linting logic
|
|
227
|
+
const ast = parseResult;
|
|
162
228
|
// Use universal rules for all components in the new pattern
|
|
163
229
|
let rules = this.universalComponentRules;
|
|
164
230
|
// Filter rules based on component type and appliesTo property
|
|
@@ -1027,6 +1093,32 @@ const { ModelTreeView, PromptTable, FilterPanel } = components;
|
|
|
1027
1093
|
// All these will be available`
|
|
1028
1094
|
});
|
|
1029
1095
|
break;
|
|
1096
|
+
case 'component-usage-without-destructuring':
|
|
1097
|
+
suggestions.push({
|
|
1098
|
+
violation: violation.rule,
|
|
1099
|
+
suggestion: 'Components must be properly accessed - either destructure from components prop or use dot notation',
|
|
1100
|
+
example: `// ❌ WRONG - Using component without destructuring:
|
|
1101
|
+
function MyComponent({ components }) {
|
|
1102
|
+
return <AccountList />; // Error: AccountList not destructured
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// ✅ CORRECT - Option 1: Destructure from components
|
|
1106
|
+
function MyComponent({ components }) {
|
|
1107
|
+
const { AccountList } = components;
|
|
1108
|
+
return <AccountList />;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// ✅ CORRECT - Option 2: Use dot notation
|
|
1112
|
+
function MyComponent({ components }) {
|
|
1113
|
+
return <components.AccountList />;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ✅ CORRECT - Option 3: Destructure in function parameters
|
|
1117
|
+
function MyComponent({ components: { AccountList } }) {
|
|
1118
|
+
return <AccountList />;
|
|
1119
|
+
}`
|
|
1120
|
+
});
|
|
1121
|
+
break;
|
|
1030
1122
|
case 'unsafe-array-access':
|
|
1031
1123
|
suggestions.push({
|
|
1032
1124
|
violation: violation.rule,
|
|
@@ -1595,6 +1687,40 @@ setData(queryResult.Results || []); // NOT queryResult directly!
|
|
|
1595
1687
|
}
|
|
1596
1688
|
return suggestions;
|
|
1597
1689
|
}
|
|
1690
|
+
static generateSyntaxErrorSuggestions(violations) {
|
|
1691
|
+
const suggestions = [];
|
|
1692
|
+
for (const violation of violations) {
|
|
1693
|
+
if (violation.message.includes('Unterminated string')) {
|
|
1694
|
+
suggestions.push({
|
|
1695
|
+
violation: violation.rule,
|
|
1696
|
+
suggestion: 'Check that all string literals are properly closed with matching quotes',
|
|
1697
|
+
example: 'Template literals with interpolation must use backticks: `text ${variable} text`'
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
else if (violation.message.includes('Unexpected token') || violation.message.includes('export')) {
|
|
1701
|
+
suggestions.push({
|
|
1702
|
+
violation: violation.rule,
|
|
1703
|
+
suggestion: 'Ensure all code is within the component function body',
|
|
1704
|
+
example: 'Remove any export statements or code outside the function definition'
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
else if (violation.message.includes('import') && violation.message.includes('top level')) {
|
|
1708
|
+
suggestions.push({
|
|
1709
|
+
violation: violation.rule,
|
|
1710
|
+
suggestion: 'Import statements are not allowed in components - use props instead',
|
|
1711
|
+
example: 'Access libraries through props: const { React, MaterialUI } = props.components'
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
suggestions.push({
|
|
1716
|
+
violation: violation.rule,
|
|
1717
|
+
suggestion: 'Fix the syntax error before the component can be compiled',
|
|
1718
|
+
example: 'Review the code at the specified line and column for syntax issues'
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return suggestions;
|
|
1723
|
+
}
|
|
1598
1724
|
/**
|
|
1599
1725
|
* Apply library-specific lint rules based on ComponentLibrary LintRules field
|
|
1600
1726
|
*/
|
|
@@ -2028,34 +2154,69 @@ ComponentLinter.universalComponentRules = [
|
|
|
2028
2154
|
appliesTo: 'all',
|
|
2029
2155
|
test: (ast, componentName, componentSpec) => {
|
|
2030
2156
|
const violations = [];
|
|
2157
|
+
// Track if we're inside the main function and where it ends
|
|
2158
|
+
let mainFunctionEnd = 0;
|
|
2159
|
+
// First pass: find the main component function
|
|
2160
|
+
(0, traverse_1.default)(ast, {
|
|
2161
|
+
FunctionDeclaration(path) {
|
|
2162
|
+
if (path.node.id?.name === componentName) {
|
|
2163
|
+
mainFunctionEnd = path.node.loc?.end.line || 0;
|
|
2164
|
+
path.stop(); // Stop traversing once we find it
|
|
2165
|
+
}
|
|
2166
|
+
},
|
|
2167
|
+
FunctionExpression(path) {
|
|
2168
|
+
// Check for function expressions assigned to const/let/var
|
|
2169
|
+
const parent = path.parent;
|
|
2170
|
+
if (t.isVariableDeclarator(parent) &&
|
|
2171
|
+
t.isIdentifier(parent.id) &&
|
|
2172
|
+
parent.id.name === componentName) {
|
|
2173
|
+
mainFunctionEnd = path.node.loc?.end.line || 0;
|
|
2174
|
+
path.stop();
|
|
2175
|
+
}
|
|
2176
|
+
},
|
|
2177
|
+
ArrowFunctionExpression(path) {
|
|
2178
|
+
// Check for arrow functions assigned to const/let/var
|
|
2179
|
+
const parent = path.parent;
|
|
2180
|
+
if (t.isVariableDeclarator(parent) &&
|
|
2181
|
+
t.isIdentifier(parent.id) &&
|
|
2182
|
+
parent.id.name === componentName) {
|
|
2183
|
+
mainFunctionEnd = path.node.loc?.end.line || 0;
|
|
2184
|
+
path.stop();
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
// Second pass: check for export statements
|
|
2031
2189
|
(0, traverse_1.default)(ast, {
|
|
2032
2190
|
ExportNamedDeclaration(path) {
|
|
2191
|
+
const line = path.node.loc?.start.line || 0;
|
|
2033
2192
|
violations.push({
|
|
2034
2193
|
rule: 'no-export-statements',
|
|
2035
2194
|
severity: 'critical',
|
|
2036
|
-
line:
|
|
2195
|
+
line: line,
|
|
2037
2196
|
column: path.node.loc?.start.column || 0,
|
|
2038
|
-
message: `Component "${componentName}" contains an export statement. Interactive components are self-contained and cannot export values.`,
|
|
2197
|
+
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
2198
|
code: path.toString().substring(0, 100)
|
|
2040
2199
|
});
|
|
2041
2200
|
},
|
|
2042
2201
|
ExportDefaultDeclaration(path) {
|
|
2202
|
+
const line = path.node.loc?.start.line || 0;
|
|
2043
2203
|
violations.push({
|
|
2044
2204
|
rule: 'no-export-statements',
|
|
2045
2205
|
severity: 'critical',
|
|
2046
|
-
line:
|
|
2206
|
+
line: line,
|
|
2047
2207
|
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.`,
|
|
2208
|
+
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
2209
|
code: path.toString().substring(0, 100)
|
|
2050
2210
|
});
|
|
2051
2211
|
},
|
|
2052
2212
|
ExportAllDeclaration(path) {
|
|
2213
|
+
const line = path.node.loc?.start.line || 0;
|
|
2053
2214
|
violations.push({
|
|
2054
2215
|
rule: 'no-export-statements',
|
|
2055
2216
|
severity: 'critical',
|
|
2056
|
-
line:
|
|
2217
|
+
line: line,
|
|
2057
2218
|
column: path.node.loc?.start.column || 0,
|
|
2058
|
-
message: `Component "${componentName}" contains an export * statement. Interactive components are self-contained and cannot export values.`,
|
|
2219
|
+
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
2220
|
code: path.toString().substring(0, 100)
|
|
2060
2221
|
});
|
|
2061
2222
|
}
|
|
@@ -2394,7 +2555,8 @@ ComponentLinter.universalComponentRules = [
|
|
|
2394
2555
|
const libraryMap = new Map();
|
|
2395
2556
|
if (componentSpec?.libraries) {
|
|
2396
2557
|
for (const lib of componentSpec.libraries) {
|
|
2397
|
-
|
|
2558
|
+
// Skip empty library objects or those without required fields
|
|
2559
|
+
if (lib.globalVariable && lib.name) {
|
|
2398
2560
|
// Store both the library name and globalVariable for lookup
|
|
2399
2561
|
libraryMap.set(lib.name.toLowerCase(), lib.globalVariable);
|
|
2400
2562
|
libraryMap.set(lib.globalVariable.toLowerCase(), lib.globalVariable);
|
|
@@ -2695,8 +2857,11 @@ ComponentLinter.universalComponentRules = [
|
|
|
2695
2857
|
const libraryGlobals = new Map();
|
|
2696
2858
|
if (componentSpec?.libraries) {
|
|
2697
2859
|
for (const lib of componentSpec.libraries) {
|
|
2698
|
-
//
|
|
2699
|
-
|
|
2860
|
+
// Skip empty library objects or those without required fields
|
|
2861
|
+
if (lib.name && lib.globalVariable) {
|
|
2862
|
+
// Store both the exact name and lowercase for comparison
|
|
2863
|
+
libraryGlobals.set(lib.name.toLowerCase(), lib.globalVariable);
|
|
2864
|
+
}
|
|
2700
2865
|
}
|
|
2701
2866
|
}
|
|
2702
2867
|
(0, traverse_1.default)(ast, {
|
|
@@ -2879,6 +3044,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2879
3044
|
// Track JSX element usage
|
|
2880
3045
|
JSXElement(path) {
|
|
2881
3046
|
const openingElement = path.node.openingElement;
|
|
3047
|
+
// Check for direct usage (e.g., <ComponentName>)
|
|
2882
3048
|
if (t.isJSXIdentifier(openingElement.name) && /^[A-Z]/.test(openingElement.name.name)) {
|
|
2883
3049
|
const componentName = openingElement.name.name;
|
|
2884
3050
|
// Only track if it's from our destructured components
|
|
@@ -2886,6 +3052,18 @@ ComponentLinter.universalComponentRules = [
|
|
|
2886
3052
|
componentsUsedInJSX.add(componentName);
|
|
2887
3053
|
}
|
|
2888
3054
|
}
|
|
3055
|
+
// Also check for components.X pattern (e.g., <components.ComponentName>)
|
|
3056
|
+
if (t.isJSXMemberExpression(openingElement.name)) {
|
|
3057
|
+
if (t.isJSXIdentifier(openingElement.name.object) &&
|
|
3058
|
+
openingElement.name.object.name === 'components' &&
|
|
3059
|
+
t.isJSXIdentifier(openingElement.name.property)) {
|
|
3060
|
+
const componentName = openingElement.name.property.name;
|
|
3061
|
+
// Track usage of components accessed via dot notation
|
|
3062
|
+
if (componentsFromProps.has(componentName)) {
|
|
3063
|
+
componentsUsedInJSX.add(componentName);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
2889
3067
|
}
|
|
2890
3068
|
});
|
|
2891
3069
|
// Only check if we found a components prop
|
|
@@ -2905,6 +3083,75 @@ ComponentLinter.universalComponentRules = [
|
|
|
2905
3083
|
return violations;
|
|
2906
3084
|
}
|
|
2907
3085
|
},
|
|
3086
|
+
{
|
|
3087
|
+
name: 'component-not-in-dependencies',
|
|
3088
|
+
appliesTo: 'all',
|
|
3089
|
+
test: (ast, componentName, componentSpec) => {
|
|
3090
|
+
const violations = [];
|
|
3091
|
+
// Get the list of available component names from dependencies
|
|
3092
|
+
const availableComponents = new Set();
|
|
3093
|
+
if (componentSpec?.dependencies) {
|
|
3094
|
+
for (const dep of componentSpec.dependencies) {
|
|
3095
|
+
if (dep.name) {
|
|
3096
|
+
availableComponents.add(dep.name);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
(0, traverse_1.default)(ast, {
|
|
3101
|
+
// Check for components.X usage in JSX
|
|
3102
|
+
JSXElement(path) {
|
|
3103
|
+
const openingElement = path.node.openingElement;
|
|
3104
|
+
// Check for components.X pattern (e.g., <components.Loading>)
|
|
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
|
+
// Check if this component is NOT in the dependencies
|
|
3111
|
+
if (!availableComponents.has(componentName)) {
|
|
3112
|
+
violations.push({
|
|
3113
|
+
rule: 'component-not-in-dependencies',
|
|
3114
|
+
severity: 'critical',
|
|
3115
|
+
line: openingElement.loc?.start.line || 0,
|
|
3116
|
+
column: openingElement.loc?.start.column || 0,
|
|
3117
|
+
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.`,
|
|
3118
|
+
code: `<components.${componentName}>`
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
},
|
|
3124
|
+
// Also check for components.X usage in JavaScript expressions
|
|
3125
|
+
MemberExpression(path) {
|
|
3126
|
+
if (t.isIdentifier(path.node.object) &&
|
|
3127
|
+
path.node.object.name === 'components' &&
|
|
3128
|
+
t.isIdentifier(path.node.property)) {
|
|
3129
|
+
const componentName = path.node.property.name;
|
|
3130
|
+
// Skip if this is a method call like components.hasOwnProperty
|
|
3131
|
+
const parent = path.parent;
|
|
3132
|
+
if (t.isCallExpression(parent) && parent.callee === path.node) {
|
|
3133
|
+
// Check if it looks like a component (starts with uppercase)
|
|
3134
|
+
if (!/^[A-Z]/.test(componentName)) {
|
|
3135
|
+
return; // Skip built-in methods
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
// Check if this component is NOT in the dependencies
|
|
3139
|
+
if (/^[A-Z]/.test(componentName) && !availableComponents.has(componentName)) {
|
|
3140
|
+
violations.push({
|
|
3141
|
+
rule: 'component-not-in-dependencies',
|
|
3142
|
+
severity: 'critical',
|
|
3143
|
+
line: path.node.loc?.start.line || 0,
|
|
3144
|
+
column: path.node.loc?.start.column || 0,
|
|
3145
|
+
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.`,
|
|
3146
|
+
code: `components.${componentName}`
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
return violations;
|
|
3153
|
+
}
|
|
3154
|
+
},
|
|
2908
3155
|
// DISABLED: Consolidated into unsafe-array-operations rule
|
|
2909
3156
|
// {
|
|
2910
3157
|
// name: 'unsafe-array-access',
|
|
@@ -3697,7 +3944,23 @@ ComponentLinter.universalComponentRules = [
|
|
|
3697
3944
|
severity: 'critical',
|
|
3698
3945
|
line: path.node.arguments[0].loc?.start.line || 0,
|
|
3699
3946
|
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'}.
|
|
3947
|
+
message: `RunViews expects an array of RunViewParams objects, not a ${t.isObjectExpression(path.node.arguments[0]) ? 'single object' : 'non-array'}.
|
|
3948
|
+
Use: RunViews([
|
|
3949
|
+
{
|
|
3950
|
+
EntityName: 'Entity1',
|
|
3951
|
+
ExtraFilter: 'IsActive = 1',
|
|
3952
|
+
Fields: 'ID, Name',
|
|
3953
|
+
StartRow: 0,
|
|
3954
|
+
MaxRows: 50
|
|
3955
|
+
},
|
|
3956
|
+
{
|
|
3957
|
+
EntityName: 'Entity2',
|
|
3958
|
+
OrderBy: 'CreatedAt DESC',
|
|
3959
|
+
StartRow: 0,
|
|
3960
|
+
MaxRows: 100
|
|
3961
|
+
}
|
|
3962
|
+
])
|
|
3963
|
+
Each object supports: EntityName, ExtraFilter, Fields, OrderBy, MaxRows, StartRow, ResultType`,
|
|
3701
3964
|
code: path.toString().substring(0, 100)
|
|
3702
3965
|
});
|
|
3703
3966
|
}
|
|
@@ -3718,7 +3981,16 @@ ComponentLinter.universalComponentRules = [
|
|
|
3718
3981
|
severity: 'critical',
|
|
3719
3982
|
line: path.node.arguments[0].loc?.start.line || 0,
|
|
3720
3983
|
column: path.node.arguments[0].loc?.start.column || 0,
|
|
3721
|
-
message: `RunView expects a RunViewParams object, not ${argType === 'array' ? 'an' : 'a'} ${argType}.
|
|
3984
|
+
message: `RunView expects a RunViewParams object, not ${argType === 'array' ? 'an' : 'a'} ${argType}.
|
|
3985
|
+
Use: RunView({
|
|
3986
|
+
EntityName: 'YourEntity',
|
|
3987
|
+
ExtraFilter: 'Status = "Active"', // Optional WHERE clause
|
|
3988
|
+
Fields: 'ID, Name, Status', // Optional columns to return
|
|
3989
|
+
OrderBy: 'Name ASC', // Optional sort
|
|
3990
|
+
StartRow: 0, // Optional offset (0-based)
|
|
3991
|
+
MaxRows: 100 // Optional limit
|
|
3992
|
+
})
|
|
3993
|
+
Valid properties: EntityName, ExtraFilter, Fields, OrderBy, MaxRows, StartRow, ResultType`,
|
|
3722
3994
|
code: path.toString().substring(0, 100)
|
|
3723
3995
|
});
|
|
3724
3996
|
}
|
|
@@ -3804,7 +4076,15 @@ ComponentLinter.universalComponentRules = [
|
|
|
3804
4076
|
severity: 'critical',
|
|
3805
4077
|
line: path.node.loc?.start.line || 0,
|
|
3806
4078
|
column: path.node.loc?.start.column || 0,
|
|
3807
|
-
message: `RunQuery requires a RunQueryParams object as the first parameter.
|
|
4079
|
+
message: `RunQuery requires a RunQueryParams object as the first parameter.
|
|
4080
|
+
Use: RunQuery({
|
|
4081
|
+
QueryName: 'YourQuery', // Or use QueryID: 'uuid'
|
|
4082
|
+
Parameters: { // Optional query parameters
|
|
4083
|
+
param1: 'value1'
|
|
4084
|
+
},
|
|
4085
|
+
StartRow: 0, // Optional offset (0-based)
|
|
4086
|
+
MaxRows: 100 // Optional limit
|
|
4087
|
+
})`,
|
|
3808
4088
|
code: `RunQuery()`
|
|
3809
4089
|
});
|
|
3810
4090
|
}
|
|
@@ -3818,7 +4098,17 @@ ComponentLinter.universalComponentRules = [
|
|
|
3818
4098
|
severity: 'critical',
|
|
3819
4099
|
line: path.node.arguments[0].loc?.start.line || 0,
|
|
3820
4100
|
column: path.node.arguments[0].loc?.start.column || 0,
|
|
3821
|
-
message: `RunQuery expects a RunQueryParams object, not a ${argType}.
|
|
4101
|
+
message: `RunQuery expects a RunQueryParams object, not a ${argType}.
|
|
4102
|
+
Use: RunQuery({
|
|
4103
|
+
QueryName: 'YourQuery', // Or use QueryID: 'uuid'
|
|
4104
|
+
Parameters: { // Optional query parameters
|
|
4105
|
+
startDate: '2024-01-01',
|
|
4106
|
+
endDate: '2024-12-31'
|
|
4107
|
+
},
|
|
4108
|
+
StartRow: 0, // Optional offset (0-based)
|
|
4109
|
+
MaxRows: 100 // Optional limit
|
|
4110
|
+
})
|
|
4111
|
+
Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxRows, StartRow, ForceAuditLog, AuditLogDescription`,
|
|
3822
4112
|
code: path.toString().substring(0, 100)
|
|
3823
4113
|
});
|
|
3824
4114
|
}
|
|
@@ -4776,13 +5066,12 @@ ComponentLinter.universalComponentRules = [
|
|
|
4776
5066
|
}
|
|
4777
5067
|
}
|
|
4778
5068
|
|
|
4779
|
-
// Track dependency components (
|
|
5069
|
+
// Track dependency components (NOT auto-destructured - must be manually destructured or accessed via components.X)
|
|
4780
5070
|
if (componentSpec?.dependencies) {
|
|
4781
5071
|
for (const dep of componentSpec.dependencies) {
|
|
4782
5072
|
if (dep.name) {
|
|
4783
5073
|
componentsFromProp.add(dep.name);
|
|
4784
|
-
//
|
|
4785
|
-
availableIdentifiers.add(dep.name);
|
|
5074
|
+
// DO NOT add to availableIdentifiers - components must be destructured first
|
|
4786
5075
|
}
|
|
4787
5076
|
}
|
|
4788
5077
|
}
|
|
@@ -5132,88 +5421,56 @@ ComponentLinter.universalComponentRules = [
|
|
|
5132
5421
|
appliesTo: 'all',
|
|
5133
5422
|
test: (ast, componentName, componentSpec) => {
|
|
5134
5423
|
const violations = [];
|
|
5135
|
-
//
|
|
5136
|
-
const
|
|
5137
|
-
(0, traverse_1.default)(ast, {
|
|
5138
|
-
// First pass: identify RunView/RunQuery calls and their assigned variables
|
|
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
|
|
5424
|
+
// Array methods that would fail on a result object - keep for smart error detection
|
|
5425
|
+
const arrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'sort', 'concat'];
|
|
5175
5426
|
(0, traverse_1.default)(ast, {
|
|
5176
|
-
// Check for direct array operations
|
|
5427
|
+
// Check for direct array operations on RunView/RunQuery results
|
|
5177
5428
|
CallExpression(path) {
|
|
5178
|
-
// Check for array methods being called on result objects
|
|
5179
5429
|
if (t.isMemberExpression(path.node.callee) &&
|
|
5180
5430
|
t.isIdentifier(path.node.callee.object) &&
|
|
5181
5431
|
t.isIdentifier(path.node.callee.property)) {
|
|
5182
5432
|
const objName = path.node.callee.object.name;
|
|
5183
5433
|
const methodName = path.node.callee.property.name;
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
const
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5434
|
+
if (arrayMethods.includes(methodName)) {
|
|
5435
|
+
// Use proper variable tracing instead of naive name matching
|
|
5436
|
+
const isFromRunView = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
|
|
5437
|
+
const isFromRunQuery = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
|
|
5438
|
+
if (isFromRunView || isFromRunQuery) {
|
|
5439
|
+
const methodType = isFromRunView ? 'RunView' : 'RunQuery';
|
|
5440
|
+
const ruleName = isFromRunView ? 'runview-result-invalid-usage' : 'runquery-result-invalid-usage';
|
|
5441
|
+
violations.push({
|
|
5442
|
+
rule: ruleName,
|
|
5443
|
+
severity: 'critical',
|
|
5444
|
+
line: path.node.loc?.start.line || 0,
|
|
5445
|
+
column: path.node.loc?.start.column || 0,
|
|
5446
|
+
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.`,
|
|
5447
|
+
code: `${objName}.${methodName}(...)`
|
|
5448
|
+
});
|
|
5449
|
+
}
|
|
5196
5450
|
}
|
|
5197
5451
|
}
|
|
5198
5452
|
},
|
|
5199
5453
|
// Check for direct usage in setState or as function arguments
|
|
5200
5454
|
Identifier(path) {
|
|
5201
5455
|
const varName = path.node.name;
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5456
|
+
const parent = path.parent;
|
|
5457
|
+
// Use proper variable tracing
|
|
5458
|
+
const isFromRunView = ComponentLinter.isVariableFromRunQueryOrView(path, varName, 'RunView');
|
|
5459
|
+
const isFromRunQuery = ComponentLinter.isVariableFromRunQueryOrView(path, varName, 'RunQuery');
|
|
5460
|
+
if (isFromRunView || isFromRunQuery) {
|
|
5461
|
+
const methodType = isFromRunView ? 'RunView' : 'RunQuery';
|
|
5462
|
+
const ruleName = isFromRunView ? 'runview-result-invalid-usage' : 'runquery-result-invalid-usage';
|
|
5205
5463
|
// Check if being passed to setState-like functions
|
|
5206
5464
|
if (t.isCallExpression(parent) && path.node === parent.arguments[0]) {
|
|
5207
5465
|
const callee = parent.callee;
|
|
5208
5466
|
// Check for setState patterns
|
|
5209
5467
|
if (t.isIdentifier(callee) && /^set[A-Z]/.test(callee.name)) {
|
|
5210
|
-
// Likely a setState function
|
|
5211
5468
|
violations.push({
|
|
5212
|
-
rule:
|
|
5469
|
+
rule: ruleName,
|
|
5213
5470
|
severity: 'critical',
|
|
5214
5471
|
line: path.node.loc?.start.line || 0,
|
|
5215
5472
|
column: path.node.loc?.start.column || 0,
|
|
5216
|
-
message: `Passing ${
|
|
5473
|
+
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
5474
|
code: `${callee.name}(${varName})`
|
|
5218
5475
|
});
|
|
5219
5476
|
}
|
|
@@ -5222,11 +5479,11 @@ ComponentLinter.universalComponentRules = [
|
|
|
5222
5479
|
const methodName = callee.property.name;
|
|
5223
5480
|
if (methodName === 'concat' || methodName === 'push' || methodName === 'unshift') {
|
|
5224
5481
|
violations.push({
|
|
5225
|
-
rule:
|
|
5482
|
+
rule: ruleName,
|
|
5226
5483
|
severity: 'critical',
|
|
5227
5484
|
line: path.node.loc?.start.line || 0,
|
|
5228
5485
|
column: path.node.loc?.start.column || 0,
|
|
5229
|
-
message: `Passing ${
|
|
5486
|
+
message: `Passing ${methodType} result to array method. Use "${varName}.Results" instead of "${varName}".`,
|
|
5230
5487
|
code: `...${methodName}(${varName})`
|
|
5231
5488
|
});
|
|
5232
5489
|
}
|
|
@@ -5243,16 +5500,50 @@ ComponentLinter.universalComponentRules = [
|
|
|
5243
5500
|
// Pattern: Array.isArray(result) ? result : []
|
|
5244
5501
|
if (parent.test.arguments[0] === path.node && parent.consequent === path.node) {
|
|
5245
5502
|
violations.push({
|
|
5246
|
-
rule:
|
|
5503
|
+
rule: ruleName,
|
|
5247
5504
|
severity: 'high',
|
|
5248
5505
|
line: path.node.loc?.start.line || 0,
|
|
5249
5506
|
column: path.node.loc?.start.column || 0,
|
|
5250
|
-
message: `${
|
|
5507
|
+
message: `${methodType} result is never an array. Use "${varName}.Results || []" instead of "Array.isArray(${varName}) ? ${varName} : []".`,
|
|
5251
5508
|
code: `Array.isArray(${varName}) ? ${varName} : []`
|
|
5252
5509
|
});
|
|
5253
5510
|
}
|
|
5254
5511
|
}
|
|
5255
5512
|
}
|
|
5513
|
+
},
|
|
5514
|
+
// Check for invalid property access on RunView/RunQuery results
|
|
5515
|
+
MemberExpression(path) {
|
|
5516
|
+
if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
|
|
5517
|
+
const objName = path.node.object.name;
|
|
5518
|
+
const propName = path.node.property.name;
|
|
5519
|
+
// Use proper variable tracing
|
|
5520
|
+
const isFromRunView = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
|
|
5521
|
+
const isFromRunQuery = ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
|
|
5522
|
+
if (isFromRunView || isFromRunQuery) {
|
|
5523
|
+
const isValidViewProp = runViewResultProps.includes(propName);
|
|
5524
|
+
const isValidQueryProp = runQueryResultProps.includes(propName);
|
|
5525
|
+
if (isFromRunQuery && !isValidQueryProp) {
|
|
5526
|
+
violations.push({
|
|
5527
|
+
rule: 'runquery-result-invalid-property',
|
|
5528
|
+
severity: 'critical',
|
|
5529
|
+
line: path.node.loc?.start.line || 0,
|
|
5530
|
+
column: path.node.loc?.start.column || 0,
|
|
5531
|
+
message: `Invalid property "${propName}" on RunQuery result. Valid properties: ${runQueryResultProps.join(', ')}.`,
|
|
5532
|
+
code: `${objName}.${propName}`
|
|
5533
|
+
});
|
|
5534
|
+
}
|
|
5535
|
+
else if (isFromRunView && !isValidViewProp) {
|
|
5536
|
+
violations.push({
|
|
5537
|
+
rule: 'runview-result-invalid-property',
|
|
5538
|
+
severity: 'critical',
|
|
5539
|
+
line: path.node.loc?.start.line || 0,
|
|
5540
|
+
column: path.node.loc?.start.column || 0,
|
|
5541
|
+
message: `Invalid property "${propName}" on RunView result. Valid properties: ${runViewResultProps.join(', ')}.`,
|
|
5542
|
+
code: `${objName}.${propName}`
|
|
5543
|
+
});
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5256
5547
|
}
|
|
5257
5548
|
});
|
|
5258
5549
|
return violations;
|
|
@@ -5263,31 +5554,9 @@ ComponentLinter.universalComponentRules = [
|
|
|
5263
5554
|
appliesTo: 'all',
|
|
5264
5555
|
test: (ast, componentName, componentSpec) => {
|
|
5265
5556
|
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
|
-
]);
|
|
5557
|
+
// Use shared property arrays from top of file - ensures consistency
|
|
5558
|
+
const validRunQueryResultProps = new Set(runQueryResultProps);
|
|
5559
|
+
const validRunViewResultProps = new Set(runViewResultProps);
|
|
5291
5560
|
// Map of common incorrect properties to the correct property
|
|
5292
5561
|
const incorrectToCorrectMap = {
|
|
5293
5562
|
'data': 'Results',
|
|
@@ -5494,66 +5763,257 @@ ComponentLinter.universalComponentRules = [
|
|
|
5494
5763
|
}
|
|
5495
5764
|
},
|
|
5496
5765
|
{
|
|
5497
|
-
name: '
|
|
5766
|
+
name: 'validate-runview-runquery-result-access',
|
|
5498
5767
|
appliesTo: 'all',
|
|
5499
5768
|
test: (ast, componentName, componentSpec) => {
|
|
5500
5769
|
const violations = [];
|
|
5501
|
-
//
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
function findClosestMatch(target, candidates) {
|
|
5520
|
-
if (candidates.length === 0)
|
|
5521
|
-
return null;
|
|
5522
|
-
// Simple Levenshtein distance implementation
|
|
5523
|
-
function levenshtein(a, b) {
|
|
5524
|
-
const matrix = [];
|
|
5525
|
-
for (let i = 0; i <= b.length; i++) {
|
|
5526
|
-
matrix[i] = [i];
|
|
5527
|
-
}
|
|
5528
|
-
for (let j = 0; j <= a.length; j++) {
|
|
5529
|
-
matrix[0][j] = j;
|
|
5530
|
-
}
|
|
5531
|
-
for (let i = 1; i <= b.length; i++) {
|
|
5532
|
-
for (let j = 1; j <= a.length; j++) {
|
|
5533
|
-
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
5534
|
-
matrix[i][j] = matrix[i - 1][j - 1];
|
|
5770
|
+
// Track variables that hold RunView/RunQuery results with their actual names
|
|
5771
|
+
const resultVariables = new Map();
|
|
5772
|
+
// First pass: identify all RunView/RunQuery calls and their assigned variables
|
|
5773
|
+
(0, traverse_1.default)(ast, {
|
|
5774
|
+
AwaitExpression(path) {
|
|
5775
|
+
const callExpr = path.node.argument;
|
|
5776
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
5777
|
+
const callee = callExpr.callee;
|
|
5778
|
+
// Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
|
|
5779
|
+
if (t.isMemberExpression(callee.object) &&
|
|
5780
|
+
t.isIdentifier(callee.object.object) &&
|
|
5781
|
+
callee.object.object.name === 'utilities' &&
|
|
5782
|
+
t.isIdentifier(callee.object.property)) {
|
|
5783
|
+
const subObject = callee.object.property.name;
|
|
5784
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
5785
|
+
let methodType = null;
|
|
5786
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
5787
|
+
methodType = method;
|
|
5535
5788
|
}
|
|
5536
|
-
else {
|
|
5537
|
-
|
|
5789
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
5790
|
+
methodType = 'RunQuery';
|
|
5791
|
+
}
|
|
5792
|
+
if (methodType) {
|
|
5793
|
+
// Check if this is being assigned to a variable
|
|
5794
|
+
const parent = path.parent;
|
|
5795
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
5796
|
+
// const result = await utilities.rv.RunView(...)
|
|
5797
|
+
resultVariables.set(parent.id.name, {
|
|
5798
|
+
line: parent.id.loc?.start.line || 0,
|
|
5799
|
+
column: parent.id.loc?.start.column || 0,
|
|
5800
|
+
method: methodType,
|
|
5801
|
+
varName: parent.id.name
|
|
5802
|
+
});
|
|
5803
|
+
}
|
|
5804
|
+
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
5805
|
+
// result = await utilities.rv.RunView(...)
|
|
5806
|
+
resultVariables.set(parent.left.name, {
|
|
5807
|
+
line: parent.left.loc?.start.line || 0,
|
|
5808
|
+
column: parent.left.loc?.start.column || 0,
|
|
5809
|
+
method: methodType,
|
|
5810
|
+
varName: parent.left.name
|
|
5811
|
+
});
|
|
5812
|
+
}
|
|
5538
5813
|
}
|
|
5539
5814
|
}
|
|
5540
5815
|
}
|
|
5541
|
-
return matrix[b.length][a.length];
|
|
5542
|
-
}
|
|
5543
|
-
// Find the closest match
|
|
5544
|
-
let bestMatch = '';
|
|
5545
|
-
let bestDistance = Infinity;
|
|
5546
|
-
for (const candidate of candidates) {
|
|
5547
|
-
const distance = levenshtein(target.toLowerCase(), candidate.toLowerCase());
|
|
5548
|
-
if (distance < bestDistance && distance <= 3) { // Max distance of 3 for suggestions
|
|
5549
|
-
bestDistance = distance;
|
|
5550
|
-
bestMatch = candidate;
|
|
5551
|
-
}
|
|
5552
5816
|
}
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5817
|
+
});
|
|
5818
|
+
// Second pass: check for incorrect usage patterns
|
|
5819
|
+
(0, traverse_1.default)(ast, {
|
|
5820
|
+
// Check for .length property access on result objects
|
|
5821
|
+
MemberExpression(path) {
|
|
5822
|
+
if (t.isIdentifier(path.node.object) &&
|
|
5823
|
+
t.isIdentifier(path.node.property) &&
|
|
5824
|
+
path.node.property.name === 'length') {
|
|
5825
|
+
const objName = path.node.object.name;
|
|
5826
|
+
if (resultVariables.has(objName)) {
|
|
5827
|
+
const resultInfo = resultVariables.get(objName);
|
|
5828
|
+
violations.push({
|
|
5829
|
+
rule: 'validate-runview-runquery-result-access',
|
|
5830
|
+
severity: 'critical',
|
|
5831
|
+
line: path.node.loc?.start.line || 0,
|
|
5832
|
+
column: path.node.loc?.start.column || 0,
|
|
5833
|
+
message: `Cannot check .length on ${resultInfo.method} result directly. ${resultInfo.method} returns an object with Success and Results properties.
|
|
5834
|
+
Correct pattern:
|
|
5835
|
+
if (${resultInfo.varName}?.Success && ${resultInfo.varName}?.Results?.length > 0) {
|
|
5836
|
+
// Process ${resultInfo.varName}.Results array
|
|
5837
|
+
}`,
|
|
5838
|
+
code: `${objName}.length`
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
},
|
|
5843
|
+
// Check for incorrect conditional checks
|
|
5844
|
+
IfStatement(path) {
|
|
5845
|
+
const test = path.node.test;
|
|
5846
|
+
// Pattern: if (result) or if (result.length)
|
|
5847
|
+
if (t.isIdentifier(test)) {
|
|
5848
|
+
const varName = test.name;
|
|
5849
|
+
if (resultVariables.has(varName)) {
|
|
5850
|
+
const resultInfo = resultVariables.get(varName);
|
|
5851
|
+
// Check if they're ONLY checking the result object without .Success
|
|
5852
|
+
let checksSuccess = false;
|
|
5853
|
+
let checksResults = false;
|
|
5854
|
+
// Scan the if block to see what they're doing with the result
|
|
5855
|
+
path.traverse({
|
|
5856
|
+
MemberExpression(innerPath) {
|
|
5857
|
+
if (t.isIdentifier(innerPath.node.object) &&
|
|
5858
|
+
innerPath.node.object.name === varName) {
|
|
5859
|
+
const prop = t.isIdentifier(innerPath.node.property) ? innerPath.node.property.name : '';
|
|
5860
|
+
if (prop === 'Success')
|
|
5861
|
+
checksSuccess = true;
|
|
5862
|
+
if (prop === 'Results')
|
|
5863
|
+
checksResults = true;
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
});
|
|
5867
|
+
// If they're accessing Results without checking Success, flag it
|
|
5868
|
+
if (checksResults && !checksSuccess) {
|
|
5869
|
+
violations.push({
|
|
5870
|
+
rule: 'validate-runview-runquery-result-access',
|
|
5871
|
+
severity: 'high',
|
|
5872
|
+
line: test.loc?.start.line || 0,
|
|
5873
|
+
column: test.loc?.start.column || 0,
|
|
5874
|
+
message: `Checking ${resultInfo.method} result without verifying Success property.
|
|
5875
|
+
Correct pattern:
|
|
5876
|
+
if (${resultInfo.varName}?.Success) {
|
|
5877
|
+
const data = ${resultInfo.varName}.Results;
|
|
5878
|
+
// Process data
|
|
5879
|
+
} else {
|
|
5880
|
+
// Handle error: ${resultInfo.varName}.ErrorMessage
|
|
5881
|
+
}`,
|
|
5882
|
+
code: `if (${varName})`
|
|
5883
|
+
});
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
// Pattern: if (result?.length)
|
|
5888
|
+
if (t.isOptionalMemberExpression(test) &&
|
|
5889
|
+
t.isIdentifier(test.object) &&
|
|
5890
|
+
t.isIdentifier(test.property) &&
|
|
5891
|
+
test.property.name === 'length') {
|
|
5892
|
+
const varName = test.object.name;
|
|
5893
|
+
if (resultVariables.has(varName)) {
|
|
5894
|
+
const resultInfo = resultVariables.get(varName);
|
|
5895
|
+
violations.push({
|
|
5896
|
+
rule: 'validate-runview-runquery-result-access',
|
|
5897
|
+
severity: 'critical',
|
|
5898
|
+
line: test.loc?.start.line || 0,
|
|
5899
|
+
column: test.loc?.start.column || 0,
|
|
5900
|
+
message: `Incorrect check: "${varName}?.length" on ${resultInfo.method} result.
|
|
5901
|
+
Correct pattern:
|
|
5902
|
+
if (${resultInfo.varName}?.Success && ${resultInfo.varName}?.Results?.length > 0) {
|
|
5903
|
+
const processedData = processChartData(${resultInfo.varName}.Results);
|
|
5904
|
+
// Use processedData
|
|
5905
|
+
}`,
|
|
5906
|
+
code: `if (${varName}?.length)`
|
|
5907
|
+
});
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
},
|
|
5911
|
+
// Check for passing result directly to functions expecting arrays
|
|
5912
|
+
CallExpression(path) {
|
|
5913
|
+
const args = path.node.arguments;
|
|
5914
|
+
for (let i = 0; i < args.length; i++) {
|
|
5915
|
+
const arg = args[i];
|
|
5916
|
+
if (t.isIdentifier(arg) && resultVariables.has(arg.name)) {
|
|
5917
|
+
const resultInfo = resultVariables.get(arg.name);
|
|
5918
|
+
// Check if the function being called looks like it expects an array
|
|
5919
|
+
let funcName = '';
|
|
5920
|
+
if (t.isIdentifier(path.node.callee)) {
|
|
5921
|
+
funcName = path.node.callee.name;
|
|
5922
|
+
}
|
|
5923
|
+
else if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
|
|
5924
|
+
funcName = path.node.callee.property.name;
|
|
5925
|
+
}
|
|
5926
|
+
// Common functions that expect arrays
|
|
5927
|
+
const arrayExpectingFuncs = [
|
|
5928
|
+
'map', 'filter', 'forEach', 'reduce', 'sort', 'concat',
|
|
5929
|
+
'processChartData', 'processData', 'transformData',
|
|
5930
|
+
'setData', 'setItems', 'setResults', 'setRows'
|
|
5931
|
+
];
|
|
5932
|
+
if (arrayExpectingFuncs.some(f => funcName.toLowerCase().includes(f.toLowerCase()))) {
|
|
5933
|
+
violations.push({
|
|
5934
|
+
rule: 'validate-runview-runquery-result-access',
|
|
5935
|
+
severity: 'critical',
|
|
5936
|
+
line: arg.loc?.start.line || 0,
|
|
5937
|
+
column: arg.loc?.start.column || 0,
|
|
5938
|
+
message: `Passing ${resultInfo.method} result object directly to ${funcName}() which expects an array.
|
|
5939
|
+
Correct pattern:
|
|
5940
|
+
if (${resultInfo.varName}?.Success) {
|
|
5941
|
+
${funcName}(${resultInfo.varName}.Results);
|
|
5942
|
+
} else {
|
|
5943
|
+
console.error('${resultInfo.method} failed:', ${resultInfo.varName}?.ErrorMessage);
|
|
5944
|
+
${funcName}([]); // Provide empty array as fallback
|
|
5945
|
+
}`,
|
|
5946
|
+
code: `${funcName}(${arg.name})`
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
});
|
|
5953
|
+
return violations;
|
|
5954
|
+
}
|
|
5955
|
+
},
|
|
5956
|
+
{
|
|
5957
|
+
name: 'dependency-prop-validation',
|
|
5958
|
+
appliesTo: 'all',
|
|
5959
|
+
test: (ast, componentName, componentSpec) => {
|
|
5960
|
+
const violations = [];
|
|
5961
|
+
// Skip if no dependencies
|
|
5962
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
5963
|
+
return violations;
|
|
5964
|
+
}
|
|
5965
|
+
// Build a map of dependency components and their expected props
|
|
5966
|
+
const dependencyPropsMap = new Map();
|
|
5967
|
+
for (const dep of componentSpec.dependencies) {
|
|
5968
|
+
const requiredProps = dep.properties
|
|
5969
|
+
?.filter(p => p.required)
|
|
5970
|
+
?.map(p => p.name) || [];
|
|
5971
|
+
const allProps = dep.properties?.map(p => p.name) || [];
|
|
5972
|
+
dependencyPropsMap.set(dep.name, {
|
|
5973
|
+
required: requiredProps,
|
|
5974
|
+
all: allProps,
|
|
5975
|
+
location: dep.location || 'embedded'
|
|
5976
|
+
});
|
|
5977
|
+
}
|
|
5978
|
+
// Helper function to find closest matching prop name
|
|
5979
|
+
function findClosestMatch(target, candidates) {
|
|
5980
|
+
if (candidates.length === 0)
|
|
5981
|
+
return null;
|
|
5982
|
+
// Simple Levenshtein distance implementation
|
|
5983
|
+
function levenshtein(a, b) {
|
|
5984
|
+
const matrix = [];
|
|
5985
|
+
for (let i = 0; i <= b.length; i++) {
|
|
5986
|
+
matrix[i] = [i];
|
|
5987
|
+
}
|
|
5988
|
+
for (let j = 0; j <= a.length; j++) {
|
|
5989
|
+
matrix[0][j] = j;
|
|
5990
|
+
}
|
|
5991
|
+
for (let i = 1; i <= b.length; i++) {
|
|
5992
|
+
for (let j = 1; j <= a.length; j++) {
|
|
5993
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
5994
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
5995
|
+
}
|
|
5996
|
+
else {
|
|
5997
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
return matrix[b.length][a.length];
|
|
6002
|
+
}
|
|
6003
|
+
// Find the closest match
|
|
6004
|
+
let bestMatch = '';
|
|
6005
|
+
let bestDistance = Infinity;
|
|
6006
|
+
for (const candidate of candidates) {
|
|
6007
|
+
const distance = levenshtein(target.toLowerCase(), candidate.toLowerCase());
|
|
6008
|
+
if (distance < bestDistance && distance <= 3) { // Max distance of 3 for suggestions
|
|
6009
|
+
bestDistance = distance;
|
|
6010
|
+
bestMatch = candidate;
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
6013
|
+
return bestMatch || null;
|
|
6014
|
+
}
|
|
6015
|
+
// Standard props that are always valid (passed by the runtime)
|
|
6016
|
+
const standardProps = new Set([
|
|
5557
6017
|
'styles', 'utilities', 'components', 'callbacks',
|
|
5558
6018
|
'savedUserSettings', 'onSaveUserSettings'
|
|
5559
6019
|
]);
|
|
@@ -6074,6 +6534,611 @@ ComponentLinter.universalComponentRules = [
|
|
|
6074
6534
|
});
|
|
6075
6535
|
return violations;
|
|
6076
6536
|
}
|
|
6537
|
+
},
|
|
6538
|
+
{
|
|
6539
|
+
name: 'validate-component-references',
|
|
6540
|
+
appliesTo: 'all',
|
|
6541
|
+
test: (ast, componentName, componentSpec) => {
|
|
6542
|
+
const violations = [];
|
|
6543
|
+
// Skip if no spec or no dependencies
|
|
6544
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
6545
|
+
return violations;
|
|
6546
|
+
}
|
|
6547
|
+
// Build a set of available component names from dependencies
|
|
6548
|
+
const availableComponents = new Set();
|
|
6549
|
+
for (const dep of componentSpec.dependencies) {
|
|
6550
|
+
if (dep.location === 'embedded' && dep.name) {
|
|
6551
|
+
availableComponents.add(dep.name);
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
// If no embedded dependencies, nothing to validate
|
|
6555
|
+
if (availableComponents.size === 0) {
|
|
6556
|
+
return violations;
|
|
6557
|
+
}
|
|
6558
|
+
// Track ALL defined variables in scope (from destructuring, imports, declarations, etc.)
|
|
6559
|
+
const definedVariables = new Set();
|
|
6560
|
+
const referencedComponents = new Set();
|
|
6561
|
+
// First pass: collect all variable declarations and destructuring
|
|
6562
|
+
(0, traverse_1.default)(ast, {
|
|
6563
|
+
// Track variable declarations (const x = ...)
|
|
6564
|
+
VariableDeclarator(path) {
|
|
6565
|
+
if (t.isIdentifier(path.node.id)) {
|
|
6566
|
+
definedVariables.add(path.node.id.name);
|
|
6567
|
+
}
|
|
6568
|
+
else if (t.isObjectPattern(path.node.id)) {
|
|
6569
|
+
// Track all destructured variables
|
|
6570
|
+
const collectDestructured = (pattern) => {
|
|
6571
|
+
for (const prop of pattern.properties) {
|
|
6572
|
+
if (t.isObjectProperty(prop)) {
|
|
6573
|
+
if (t.isIdentifier(prop.value)) {
|
|
6574
|
+
definedVariables.add(prop.value.name);
|
|
6575
|
+
}
|
|
6576
|
+
else if (t.isObjectPattern(prop.value)) {
|
|
6577
|
+
collectDestructured(prop.value);
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
6581
|
+
definedVariables.add(prop.argument.name);
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
};
|
|
6585
|
+
collectDestructured(path.node.id);
|
|
6586
|
+
}
|
|
6587
|
+
else if (t.isArrayPattern(path.node.id)) {
|
|
6588
|
+
// Track array destructuring
|
|
6589
|
+
for (const elem of path.node.id.elements) {
|
|
6590
|
+
if (t.isIdentifier(elem)) {
|
|
6591
|
+
definedVariables.add(elem.name);
|
|
6592
|
+
}
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
},
|
|
6596
|
+
// Track function declarations
|
|
6597
|
+
FunctionDeclaration(path) {
|
|
6598
|
+
if (path.node.id) {
|
|
6599
|
+
definedVariables.add(path.node.id.name);
|
|
6600
|
+
}
|
|
6601
|
+
},
|
|
6602
|
+
// Track class declarations
|
|
6603
|
+
ClassDeclaration(path) {
|
|
6604
|
+
if (path.node.id) {
|
|
6605
|
+
definedVariables.add(path.node.id.name);
|
|
6606
|
+
}
|
|
6607
|
+
},
|
|
6608
|
+
// Track function parameters
|
|
6609
|
+
Function(path) {
|
|
6610
|
+
for (const param of path.node.params) {
|
|
6611
|
+
if (t.isIdentifier(param)) {
|
|
6612
|
+
definedVariables.add(param.name);
|
|
6613
|
+
}
|
|
6614
|
+
else if (t.isObjectPattern(param)) {
|
|
6615
|
+
// Track destructured parameters
|
|
6616
|
+
const collectParams = (pattern) => {
|
|
6617
|
+
for (const prop of pattern.properties) {
|
|
6618
|
+
if (t.isObjectProperty(prop)) {
|
|
6619
|
+
if (t.isIdentifier(prop.value)) {
|
|
6620
|
+
definedVariables.add(prop.value.name);
|
|
6621
|
+
}
|
|
6622
|
+
else if (t.isObjectPattern(prop.value)) {
|
|
6623
|
+
collectParams(prop.value);
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
};
|
|
6628
|
+
collectParams(param);
|
|
6629
|
+
}
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
});
|
|
6633
|
+
// Second pass: check component usage
|
|
6634
|
+
(0, traverse_1.default)(ast, {
|
|
6635
|
+
// Look for React.createElement calls
|
|
6636
|
+
CallExpression(path) {
|
|
6637
|
+
const callee = path.node.callee;
|
|
6638
|
+
// Check for React.createElement(ComponentName, ...)
|
|
6639
|
+
if (t.isMemberExpression(callee) &&
|
|
6640
|
+
t.isIdentifier(callee.object) &&
|
|
6641
|
+
callee.object.name === 'React' &&
|
|
6642
|
+
t.isIdentifier(callee.property) &&
|
|
6643
|
+
callee.property.name === 'createElement') {
|
|
6644
|
+
const firstArg = path.node.arguments[0];
|
|
6645
|
+
// If first argument is an identifier (component reference)
|
|
6646
|
+
if (t.isIdentifier(firstArg)) {
|
|
6647
|
+
const componentRef = firstArg.name;
|
|
6648
|
+
// Skip HTML elements and React built-ins
|
|
6649
|
+
if (!componentRef.match(/^[a-z]/) && componentRef !== 'Fragment') {
|
|
6650
|
+
// Only check if it's supposed to be a component dependency
|
|
6651
|
+
// and it's not defined elsewhere in the code
|
|
6652
|
+
if (availableComponents.has(componentRef)) {
|
|
6653
|
+
referencedComponents.add(componentRef);
|
|
6654
|
+
}
|
|
6655
|
+
else if (!definedVariables.has(componentRef)) {
|
|
6656
|
+
// Only complain if it's not defined anywhere
|
|
6657
|
+
const availableList = Array.from(availableComponents).sort().join(', ');
|
|
6658
|
+
const availableLibs = componentSpec?.libraries?.map(lib => lib.globalVariable).filter(Boolean).join(', ') || '';
|
|
6659
|
+
let message = `Component "${componentRef}" is not defined. Available component dependencies: ${availableList}`;
|
|
6660
|
+
if (availableLibs) {
|
|
6661
|
+
message += `. Available libraries: ${availableLibs}`;
|
|
6662
|
+
}
|
|
6663
|
+
violations.push({
|
|
6664
|
+
rule: 'validate-component-references',
|
|
6665
|
+
severity: 'critical',
|
|
6666
|
+
line: firstArg.loc?.start.line || 0,
|
|
6667
|
+
column: firstArg.loc?.start.column || 0,
|
|
6668
|
+
message: message,
|
|
6669
|
+
code: `React.createElement(${componentRef}, ...)`
|
|
6670
|
+
});
|
|
6671
|
+
}
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
},
|
|
6676
|
+
// Look for JSX elements
|
|
6677
|
+
JSXElement(path) {
|
|
6678
|
+
const openingElement = path.node.openingElement;
|
|
6679
|
+
const elementName = openingElement.name;
|
|
6680
|
+
if (t.isJSXIdentifier(elementName)) {
|
|
6681
|
+
const componentRef = elementName.name;
|
|
6682
|
+
// Skip HTML elements and fragments
|
|
6683
|
+
if (!componentRef.match(/^[a-z]/) && componentRef !== 'Fragment') {
|
|
6684
|
+
// Track if it's a known component dependency
|
|
6685
|
+
if (availableComponents.has(componentRef)) {
|
|
6686
|
+
referencedComponents.add(componentRef);
|
|
6687
|
+
}
|
|
6688
|
+
else if (!definedVariables.has(componentRef)) {
|
|
6689
|
+
// Only complain if it's not defined anywhere (not from libraries, not from declarations)
|
|
6690
|
+
const availableList = Array.from(availableComponents).sort().join(', ');
|
|
6691
|
+
const availableLibs = componentSpec?.libraries?.map(lib => lib.globalVariable).filter(Boolean).join(', ') || '';
|
|
6692
|
+
let message = `Component "${componentRef}" is not defined. Available component dependencies: ${availableList}`;
|
|
6693
|
+
if (availableLibs) {
|
|
6694
|
+
message += `. Available libraries: ${availableLibs}`;
|
|
6695
|
+
}
|
|
6696
|
+
violations.push({
|
|
6697
|
+
rule: 'validate-component-references',
|
|
6698
|
+
severity: 'critical',
|
|
6699
|
+
line: elementName.loc?.start.line || 0,
|
|
6700
|
+
column: elementName.loc?.start.column || 0,
|
|
6701
|
+
message: message,
|
|
6702
|
+
code: `<${componentRef} ... />`
|
|
6703
|
+
});
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
},
|
|
6708
|
+
// Look for destructuring from components prop specifically
|
|
6709
|
+
ObjectPattern(path) {
|
|
6710
|
+
// Check if this is destructuring from a 'components' parameter
|
|
6711
|
+
const parent = path.parent;
|
|
6712
|
+
// Check if it's a function parameter with components
|
|
6713
|
+
if ((t.isFunctionDeclaration(parent) || t.isFunctionExpression(parent) ||
|
|
6714
|
+
t.isArrowFunctionExpression(parent)) && parent.params.includes(path.node)) {
|
|
6715
|
+
// Look for components property
|
|
6716
|
+
for (const prop of path.node.properties) {
|
|
6717
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) &&
|
|
6718
|
+
prop.key.name === 'components' && t.isObjectPattern(prop.value)) {
|
|
6719
|
+
// Check each destructured component
|
|
6720
|
+
for (const componentProp of prop.value.properties) {
|
|
6721
|
+
if (t.isObjectProperty(componentProp) && t.isIdentifier(componentProp.key)) {
|
|
6722
|
+
const componentRef = componentProp.key.name;
|
|
6723
|
+
referencedComponents.add(componentRef);
|
|
6724
|
+
if (!availableComponents.has(componentRef)) {
|
|
6725
|
+
const availableList = Array.from(availableComponents).sort().join(', ');
|
|
6726
|
+
// Try to find similar names for suggestions
|
|
6727
|
+
const suggestions = Array.from(availableComponents).filter(name => name.toLowerCase().includes(componentRef.toLowerCase()) ||
|
|
6728
|
+
componentRef.toLowerCase().includes(name.toLowerCase()));
|
|
6729
|
+
let message = `Destructured component "${componentRef}" is not found in dependencies. Available components: ${availableList}`;
|
|
6730
|
+
if (suggestions.length > 0) {
|
|
6731
|
+
message += `. Did you mean: ${suggestions.join(' or ')}?`;
|
|
6732
|
+
}
|
|
6733
|
+
violations.push({
|
|
6734
|
+
rule: 'validate-component-references',
|
|
6735
|
+
severity: 'critical',
|
|
6736
|
+
line: componentProp.key.loc?.start.line || 0,
|
|
6737
|
+
column: componentProp.key.loc?.start.column || 0,
|
|
6738
|
+
message: message,
|
|
6739
|
+
code: `{ components: { ${componentRef}, ... } }`
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
}
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
});
|
|
6749
|
+
// Also warn about unused dependencies
|
|
6750
|
+
for (const depName of availableComponents) {
|
|
6751
|
+
if (!referencedComponents.has(depName)) {
|
|
6752
|
+
violations.push({
|
|
6753
|
+
rule: 'validate-component-references',
|
|
6754
|
+
severity: 'low',
|
|
6755
|
+
line: 1,
|
|
6756
|
+
column: 0,
|
|
6757
|
+
message: `Component dependency "${depName}" is defined but never used in the code.`,
|
|
6758
|
+
code: `dependencies: [..., { name: "${depName}", ... }, ...]`
|
|
6759
|
+
});
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6762
|
+
return violations;
|
|
6763
|
+
}
|
|
6764
|
+
},
|
|
6765
|
+
{
|
|
6766
|
+
name: 'unused-libraries',
|
|
6767
|
+
appliesTo: 'all',
|
|
6768
|
+
test: (ast, componentName, componentSpec) => {
|
|
6769
|
+
const violations = [];
|
|
6770
|
+
// Skip if no libraries declared
|
|
6771
|
+
if (!componentSpec?.libraries || componentSpec.libraries.length === 0) {
|
|
6772
|
+
return violations;
|
|
6773
|
+
}
|
|
6774
|
+
// Get the function body to search within
|
|
6775
|
+
let functionBody = '';
|
|
6776
|
+
(0, traverse_1.default)(ast, {
|
|
6777
|
+
FunctionDeclaration(path) {
|
|
6778
|
+
if (path.node.id && path.node.id.name === componentName) {
|
|
6779
|
+
functionBody = path.toString();
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
});
|
|
6783
|
+
// If we couldn't find the function body, use the whole code
|
|
6784
|
+
if (!functionBody) {
|
|
6785
|
+
functionBody = ast.toString ? ast.toString() : '';
|
|
6786
|
+
}
|
|
6787
|
+
// Track which libraries are used and unused
|
|
6788
|
+
const unusedLibraries = [];
|
|
6789
|
+
const usedLibraries = [];
|
|
6790
|
+
// Check each library for usage
|
|
6791
|
+
for (const lib of componentSpec.libraries) {
|
|
6792
|
+
const globalVar = lib.globalVariable;
|
|
6793
|
+
if (!globalVar)
|
|
6794
|
+
continue;
|
|
6795
|
+
// Check for various usage patterns
|
|
6796
|
+
const usagePatterns = [
|
|
6797
|
+
globalVar + '.', // Direct property access: Chart.defaults
|
|
6798
|
+
globalVar + '(', // Direct call: dayjs()
|
|
6799
|
+
'new ' + globalVar + '(', // Constructor: new Chart()
|
|
6800
|
+
globalVar + '[', // Array/property access: XLSX['utils']
|
|
6801
|
+
'= ' + globalVar, // Assignment: const myChart = Chart
|
|
6802
|
+
', ' + globalVar, // In parameter list
|
|
6803
|
+
'(' + globalVar, // Start of expression
|
|
6804
|
+
'{' + globalVar, // In object literal
|
|
6805
|
+
'<' + globalVar, // JSX component
|
|
6806
|
+
globalVar + ' ', // Followed by space (various uses)
|
|
6807
|
+
];
|
|
6808
|
+
const isUsed = usagePatterns.some(pattern => functionBody.includes(pattern));
|
|
6809
|
+
if (isUsed) {
|
|
6810
|
+
usedLibraries.push({ name: lib.name, globalVariable: globalVar });
|
|
6811
|
+
}
|
|
6812
|
+
else {
|
|
6813
|
+
unusedLibraries.push({ name: lib.name, globalVariable: globalVar });
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
// Determine severity based on usage patterns
|
|
6817
|
+
const totalLibraries = componentSpec.libraries.length;
|
|
6818
|
+
const usedCount = usedLibraries.length;
|
|
6819
|
+
if (usedCount === 0 && totalLibraries > 0) {
|
|
6820
|
+
// CRITICAL: No libraries used at all
|
|
6821
|
+
violations.push({
|
|
6822
|
+
rule: 'unused-libraries',
|
|
6823
|
+
severity: 'critical',
|
|
6824
|
+
line: 1,
|
|
6825
|
+
column: 0,
|
|
6826
|
+
message: `CRITICAL: None of the ${totalLibraries} declared libraries are used. This indicates missing core functionality.`,
|
|
6827
|
+
code: `Unused libraries: ${unusedLibraries.map(l => l.name).join(', ')}`
|
|
6828
|
+
});
|
|
6829
|
+
}
|
|
6830
|
+
else if (unusedLibraries.length > 0) {
|
|
6831
|
+
// Some libraries unused, severity depends on ratio
|
|
6832
|
+
for (const lib of unusedLibraries) {
|
|
6833
|
+
const severity = totalLibraries === 1 ? 'high' : 'low';
|
|
6834
|
+
const contextMessage = totalLibraries === 1
|
|
6835
|
+
? 'This is the only declared library and it\'s not being used.'
|
|
6836
|
+
: `${usedCount} of ${totalLibraries} libraries are being used. This might be an alternative/optional library.`;
|
|
6837
|
+
violations.push({
|
|
6838
|
+
rule: 'unused-libraries',
|
|
6839
|
+
severity: severity,
|
|
6840
|
+
line: 1,
|
|
6841
|
+
column: 0,
|
|
6842
|
+
message: `Library "${lib.name}" (${lib.globalVariable}) is declared but not used. ${contextMessage}`,
|
|
6843
|
+
code: `Consider removing if not needed: { name: "${lib.name}", globalVariable: "${lib.globalVariable}" }`
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
return violations;
|
|
6848
|
+
}
|
|
6849
|
+
},
|
|
6850
|
+
{
|
|
6851
|
+
name: 'unused-component-dependencies',
|
|
6852
|
+
appliesTo: 'all',
|
|
6853
|
+
test: (ast, componentName, componentSpec) => {
|
|
6854
|
+
const violations = [];
|
|
6855
|
+
// Skip if no dependencies declared
|
|
6856
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
6857
|
+
return violations;
|
|
6858
|
+
}
|
|
6859
|
+
// Filter to only embedded components
|
|
6860
|
+
const embeddedDeps = componentSpec.dependencies.filter(dep => dep.location === 'embedded' && dep.name);
|
|
6861
|
+
if (embeddedDeps.length === 0) {
|
|
6862
|
+
return violations;
|
|
6863
|
+
}
|
|
6864
|
+
// Get the function body to search within
|
|
6865
|
+
let functionBody = '';
|
|
6866
|
+
(0, traverse_1.default)(ast, {
|
|
6867
|
+
FunctionDeclaration(path) {
|
|
6868
|
+
if (path.node.id && path.node.id.name === componentName) {
|
|
6869
|
+
functionBody = path.toString();
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
});
|
|
6873
|
+
// If we couldn't find the function body, use the whole code
|
|
6874
|
+
if (!functionBody) {
|
|
6875
|
+
functionBody = ast.toString ? ast.toString() : '';
|
|
6876
|
+
}
|
|
6877
|
+
// Check each component dependency for usage
|
|
6878
|
+
for (const dep of embeddedDeps) {
|
|
6879
|
+
const depName = dep.name;
|
|
6880
|
+
// Check for various usage patterns
|
|
6881
|
+
// Components can be used directly (if destructured) or via components object
|
|
6882
|
+
const usagePatterns = [
|
|
6883
|
+
// Direct usage (after destructuring)
|
|
6884
|
+
'<' + depName + ' ', // JSX: <AccountList />
|
|
6885
|
+
'<' + depName + '>', // JSX: <AccountList>
|
|
6886
|
+
'<' + depName + '/', // JSX self-closing: <AccountList/>
|
|
6887
|
+
depName + '(', // Direct call: AccountList()
|
|
6888
|
+
'= ' + depName, // Assignment: const List = AccountList
|
|
6889
|
+
depName + ' ||', // Fallback: AccountList || DefaultComponent
|
|
6890
|
+
depName + ' &&', // Conditional: AccountList && ...
|
|
6891
|
+
depName + ' ?', // Ternary: AccountList ? ... : ...
|
|
6892
|
+
', ' + depName, // In parameter/array list
|
|
6893
|
+
'(' + depName, // Start of expression
|
|
6894
|
+
'{' + depName, // In object literal
|
|
6895
|
+
// Via components object
|
|
6896
|
+
'components.' + depName, // Dot notation: components.AccountList
|
|
6897
|
+
"components['" + depName + "']", // Bracket notation single quotes
|
|
6898
|
+
'components["' + depName + '"]', // Bracket notation double quotes
|
|
6899
|
+
'components[`' + depName + '`]', // Bracket notation template literal
|
|
6900
|
+
'<components.' + depName, // JSX via components: <components.AccountList
|
|
6901
|
+
];
|
|
6902
|
+
const isUsed = usagePatterns.some(pattern => functionBody.includes(pattern));
|
|
6903
|
+
if (!isUsed) {
|
|
6904
|
+
violations.push({
|
|
6905
|
+
rule: 'unused-component-dependencies',
|
|
6906
|
+
severity: 'high',
|
|
6907
|
+
line: 1,
|
|
6908
|
+
column: 0,
|
|
6909
|
+
message: `Component dependency "${depName}" is declared but never used. This likely means missing functionality.`,
|
|
6910
|
+
code: `Expected usage: <${depName} /> or <components.${depName} />`
|
|
6911
|
+
});
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
return violations;
|
|
6915
|
+
}
|
|
6916
|
+
},
|
|
6917
|
+
{
|
|
6918
|
+
name: 'component-usage-without-destructuring',
|
|
6919
|
+
appliesTo: 'all',
|
|
6920
|
+
test: (ast, componentName, componentSpec) => {
|
|
6921
|
+
const violations = [];
|
|
6922
|
+
// Skip if no dependencies
|
|
6923
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
6924
|
+
return violations;
|
|
6925
|
+
}
|
|
6926
|
+
// Track dependency names
|
|
6927
|
+
const dependencyNames = new Set(componentSpec.dependencies.map(d => d.name).filter(Boolean));
|
|
6928
|
+
// Track what's been destructured from components prop
|
|
6929
|
+
const destructuredComponents = new Set();
|
|
6930
|
+
(0, traverse_1.default)(ast, {
|
|
6931
|
+
// Track destructuring from components
|
|
6932
|
+
VariableDeclarator(path) {
|
|
6933
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
6934
|
+
// Check if destructuring from 'components'
|
|
6935
|
+
if (path.node.init.name === 'components') {
|
|
6936
|
+
for (const prop of path.node.id.properties) {
|
|
6937
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
6938
|
+
const name = prop.key.name;
|
|
6939
|
+
if (dependencyNames.has(name)) {
|
|
6940
|
+
destructuredComponents.add(name);
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
},
|
|
6947
|
+
// Also check function parameter destructuring
|
|
6948
|
+
FunctionDeclaration(path) {
|
|
6949
|
+
if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
|
|
6950
|
+
const param = path.node.params[0];
|
|
6951
|
+
if (t.isObjectPattern(param)) {
|
|
6952
|
+
for (const prop of param.properties) {
|
|
6953
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'components') {
|
|
6954
|
+
// Check for nested destructuring like { components: { A, B } }
|
|
6955
|
+
if (t.isObjectPattern(prop.value)) {
|
|
6956
|
+
for (const innerProp of prop.value.properties) {
|
|
6957
|
+
if (t.isObjectProperty(innerProp) && t.isIdentifier(innerProp.key)) {
|
|
6958
|
+
const name = innerProp.key.name;
|
|
6959
|
+
if (dependencyNames.has(name)) {
|
|
6960
|
+
destructuredComponents.add(name);
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6967
|
+
}
|
|
6968
|
+
}
|
|
6969
|
+
},
|
|
6970
|
+
// Check JSX usage
|
|
6971
|
+
JSXElement(path) {
|
|
6972
|
+
const openingElement = path.node.openingElement;
|
|
6973
|
+
// Check for direct component usage (e.g., <ComponentName>)
|
|
6974
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
6975
|
+
const name = openingElement.name.name;
|
|
6976
|
+
// Check if this is one of our dependencies being used directly
|
|
6977
|
+
if (dependencyNames.has(name) && !destructuredComponents.has(name)) {
|
|
6978
|
+
violations.push({
|
|
6979
|
+
rule: 'component-usage-without-destructuring',
|
|
6980
|
+
severity: 'critical',
|
|
6981
|
+
line: openingElement.loc?.start.line || 0,
|
|
6982
|
+
column: openingElement.loc?.start.column || 0,
|
|
6983
|
+
message: `Component "${name}" used without destructuring. Either destructure it from components prop (const { ${name} } = components;) or use <components.${name} />`,
|
|
6984
|
+
code: `<${name}>`
|
|
6985
|
+
});
|
|
6986
|
+
}
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
});
|
|
6990
|
+
return violations;
|
|
6991
|
+
}
|
|
6992
|
+
},
|
|
6993
|
+
{
|
|
6994
|
+
name: 'prefer-jsx-syntax',
|
|
6995
|
+
appliesTo: 'all',
|
|
6996
|
+
test: (ast, componentName) => {
|
|
6997
|
+
const violations = [];
|
|
6998
|
+
(0, traverse_1.default)(ast, {
|
|
6999
|
+
CallExpression(path) {
|
|
7000
|
+
const callee = path.node.callee;
|
|
7001
|
+
// Check for React.createElement
|
|
7002
|
+
if (t.isMemberExpression(callee) &&
|
|
7003
|
+
t.isIdentifier(callee.object) &&
|
|
7004
|
+
callee.object.name === 'React' &&
|
|
7005
|
+
t.isIdentifier(callee.property) &&
|
|
7006
|
+
callee.property.name === 'createElement') {
|
|
7007
|
+
violations.push({
|
|
7008
|
+
rule: 'prefer-jsx-syntax',
|
|
7009
|
+
severity: 'low',
|
|
7010
|
+
line: callee.loc?.start.line || 0,
|
|
7011
|
+
column: callee.loc?.start.column || 0,
|
|
7012
|
+
message: 'Prefer JSX syntax over React.createElement for better readability',
|
|
7013
|
+
code: 'React.createElement(...) → <ComponentName ... />'
|
|
7014
|
+
});
|
|
7015
|
+
}
|
|
7016
|
+
}
|
|
7017
|
+
});
|
|
7018
|
+
return violations;
|
|
7019
|
+
}
|
|
7020
|
+
},
|
|
7021
|
+
{
|
|
7022
|
+
name: 'prefer-async-await',
|
|
7023
|
+
appliesTo: 'all',
|
|
7024
|
+
test: (ast, componentName) => {
|
|
7025
|
+
const violations = [];
|
|
7026
|
+
(0, traverse_1.default)(ast, {
|
|
7027
|
+
CallExpression(path) {
|
|
7028
|
+
const callee = path.node.callee;
|
|
7029
|
+
// Check for .then() chains
|
|
7030
|
+
if (t.isMemberExpression(callee) &&
|
|
7031
|
+
t.isIdentifier(callee.property) &&
|
|
7032
|
+
callee.property.name === 'then') {
|
|
7033
|
+
// Try to get the context of what's being chained
|
|
7034
|
+
let context = '';
|
|
7035
|
+
if (t.isMemberExpression(callee.object)) {
|
|
7036
|
+
context = ' Consider using async/await for cleaner code.';
|
|
7037
|
+
}
|
|
7038
|
+
violations.push({
|
|
7039
|
+
rule: 'prefer-async-await',
|
|
7040
|
+
severity: 'low',
|
|
7041
|
+
line: callee.property.loc?.start.line || 0,
|
|
7042
|
+
column: callee.property.loc?.start.column || 0,
|
|
7043
|
+
message: `Prefer async/await over .then() chains for better readability.${context}`,
|
|
7044
|
+
code: '.then(result => ...) → const result = await ...'
|
|
7045
|
+
});
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
});
|
|
7049
|
+
return violations;
|
|
7050
|
+
}
|
|
7051
|
+
},
|
|
7052
|
+
{
|
|
7053
|
+
name: 'single-function-only',
|
|
7054
|
+
appliesTo: 'all',
|
|
7055
|
+
test: (ast, componentName) => {
|
|
7056
|
+
const violations = [];
|
|
7057
|
+
// Count all function declarations and expressions at the top level
|
|
7058
|
+
const functionDeclarations = [];
|
|
7059
|
+
const functionExpressions = [];
|
|
7060
|
+
(0, traverse_1.default)(ast, {
|
|
7061
|
+
FunctionDeclaration(path) {
|
|
7062
|
+
// Only check top-level functions (not nested inside other functions)
|
|
7063
|
+
const parent = path.getFunctionParent();
|
|
7064
|
+
if (!parent) {
|
|
7065
|
+
const funcName = path.node.id?.name || 'anonymous';
|
|
7066
|
+
functionDeclarations.push({
|
|
7067
|
+
name: funcName,
|
|
7068
|
+
line: path.node.loc?.start.line || 0,
|
|
7069
|
+
column: path.node.loc?.start.column || 0
|
|
7070
|
+
});
|
|
7071
|
+
}
|
|
7072
|
+
},
|
|
7073
|
+
VariableDeclaration(path) {
|
|
7074
|
+
// Check for const/let/var func = function() or arrow functions at top level
|
|
7075
|
+
const parent = path.getFunctionParent();
|
|
7076
|
+
if (!parent) {
|
|
7077
|
+
for (const declarator of path.node.declarations) {
|
|
7078
|
+
if (t.isVariableDeclarator(declarator) &&
|
|
7079
|
+
(t.isFunctionExpression(declarator.init) ||
|
|
7080
|
+
t.isArrowFunctionExpression(declarator.init))) {
|
|
7081
|
+
const funcName = t.isIdentifier(declarator.id) ? declarator.id.name : 'anonymous';
|
|
7082
|
+
functionExpressions.push({
|
|
7083
|
+
name: funcName,
|
|
7084
|
+
line: declarator.loc?.start.line || 0,
|
|
7085
|
+
column: declarator.loc?.start.column || 0
|
|
7086
|
+
});
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
});
|
|
7092
|
+
const allFunctions = [...functionDeclarations, ...functionExpressions];
|
|
7093
|
+
// Check if we have more than one function
|
|
7094
|
+
if (allFunctions.length > 1) {
|
|
7095
|
+
// Find which one is the main component
|
|
7096
|
+
const mainComponentIndex = allFunctions.findIndex(f => f.name === componentName);
|
|
7097
|
+
const otherFunctions = allFunctions.filter((_, index) => index !== mainComponentIndex);
|
|
7098
|
+
violations.push({
|
|
7099
|
+
rule: 'single-function-only',
|
|
7100
|
+
severity: 'critical',
|
|
7101
|
+
line: otherFunctions[0].line,
|
|
7102
|
+
column: otherFunctions[0].column,
|
|
7103
|
+
message: `Component code must contain ONLY the main component function "${componentName}". Found ${allFunctions.length} functions: ${allFunctions.map(f => f.name).join(', ')}. Move other functions to separate component dependencies.`,
|
|
7104
|
+
code: `Remove functions: ${otherFunctions.map(f => f.name).join(', ')}`
|
|
7105
|
+
});
|
|
7106
|
+
// Add a violation for each extra function
|
|
7107
|
+
for (const func of otherFunctions) {
|
|
7108
|
+
violations.push({
|
|
7109
|
+
rule: 'single-function-only',
|
|
7110
|
+
severity: 'critical',
|
|
7111
|
+
line: func.line,
|
|
7112
|
+
column: func.column,
|
|
7113
|
+
message: `Extra function "${func.name}" not allowed. Each component must be a single function. Move this to a separate component dependency.`,
|
|
7114
|
+
code: `function ${func.name} should be a separate component`
|
|
7115
|
+
});
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
// Also check that the single function matches the component name
|
|
7119
|
+
if (allFunctions.length === 1 && allFunctions[0].name !== componentName) {
|
|
7120
|
+
violations.push({
|
|
7121
|
+
rule: 'single-function-only',
|
|
7122
|
+
severity: 'critical',
|
|
7123
|
+
line: allFunctions[0].line,
|
|
7124
|
+
column: allFunctions[0].column,
|
|
7125
|
+
message: `Component function name "${allFunctions[0].name}" does not match component name "${componentName}". The function must be named exactly as specified.`,
|
|
7126
|
+
code: `Rename function to: function ${componentName}(...)`
|
|
7127
|
+
});
|
|
7128
|
+
}
|
|
7129
|
+
// Check for no function at all
|
|
7130
|
+
if (allFunctions.length === 0) {
|
|
7131
|
+
violations.push({
|
|
7132
|
+
rule: 'single-function-only',
|
|
7133
|
+
severity: 'critical',
|
|
7134
|
+
line: 1,
|
|
7135
|
+
column: 0,
|
|
7136
|
+
message: `Component code must contain exactly one function named "${componentName}". No functions found.`,
|
|
7137
|
+
code: `Add: function ${componentName}({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) { ... }`
|
|
7138
|
+
});
|
|
7139
|
+
}
|
|
7140
|
+
return violations;
|
|
7141
|
+
}
|
|
6077
7142
|
}
|
|
6078
7143
|
];
|
|
6079
7144
|
//# sourceMappingURL=component-linter.js.map
|