@memberjunction/react-test-harness 2.93.0 → 2.95.0

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