@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.
@@ -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
- const ast = parser.parse(code, {
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: path.node.loc?.start.line || 0,
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: path.node.loc?.start.line || 0,
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: path.node.loc?.start.line || 0,
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
- if (lib.globalVariable) {
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
- // Store both the exact name and lowercase for comparison
2699
- libraryGlobals.set(lib.name.toLowerCase(), lib.globalVariable);
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'}. Use: RunViews([{ EntityName: 'Entity1' }, { EntityName: 'Entity2' }])`,
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}. Use: RunView({ EntityName: 'YourEntity' }) or for multiple use RunViews([...])`,
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. Must provide an object with either QueryID or QueryName.`,
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}. Use: RunQuery({ QueryName: 'YourQuery' }) or RunQuery({ QueryID: 'id' })`,
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
- // Build set of allowed props: standard props + componentSpec properties
4398
- const allowedProps = new Set(standardProps);
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 (these are now auto-destructured in the wrapper)
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
- // Mark as available since they're auto-destructured
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
- // Track variables that hold RunView/RunQuery results
5136
- const resultVariables = new Map();
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
- // 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
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
- // Array methods that would fail on a result object
5185
- const arrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'sort', 'concat'];
5186
- if (resultVariables.has(objName) && arrayMethods.includes(methodName)) {
5187
- const resultInfo = resultVariables.get(objName);
5188
- violations.push({
5189
- rule: 'runview-runquery-result-direct-usage',
5190
- severity: 'critical',
5191
- line: path.node.loc?.start.line || 0,
5192
- column: path.node.loc?.start.column || 0,
5193
- message: `Cannot call array method "${methodName}" directly on ${resultInfo.method} result. Use "${objName}.Results.${methodName}(...)" instead. ${resultInfo.method} returns an object with { Success, Results, ... }, not an array.`,
5194
- code: `${objName}.${methodName}(...)`
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
- if (resultVariables.has(varName)) {
5203
- const resultInfo = resultVariables.get(varName);
5204
- const parent = path.parent;
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: 'runview-runquery-result-direct-usage',
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 ${resultInfo.method} result directly to setState. Use "${varName}.Results" or check "${varName}.Success" first. ${resultInfo.method} returns { Success, Results, ErrorMessage }, not the data array.`,
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: 'runview-runquery-result-direct-usage',
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 ${resultInfo.method} result to array method. Use "${varName}.Results" instead of "${varName}".`,
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: 'runview-runquery-result-direct-usage',
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: `${resultInfo.method} result is never an array. Use "${varName}.Results || []" instead of "Array.isArray(${varName}) ? ${varName} : []".`,
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
- // Valid properties for RunQueryResult based on MJCore type definition
5267
- const validRunQueryResultProps = new Set([
5268
- 'QueryID', // string
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