@memberjunction/react-test-harness 2.95.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
@@ -99,6 +101,13 @@ const runViewResultProps = [
99
101
  'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
100
102
  ];
101
103
  class ComponentLinter {
104
+ // Get or create the styles analyzer instance
105
+ static getStylesAnalyzer() {
106
+ if (!ComponentLinter.stylesAnalyzer) {
107
+ ComponentLinter.stylesAnalyzer = new styles_type_analyzer_1.StylesTypeAnalyzer();
108
+ }
109
+ return ComponentLinter.stylesAnalyzer;
110
+ }
102
111
  // Helper method to check if a statement contains a return
103
112
  static containsReturn(node) {
104
113
  let hasReturn = false;
@@ -1683,6 +1692,46 @@ setData(queryResult.Results || []); // NOT queryResult directly!
1683
1692
  // }`
1684
1693
  });
1685
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;
1686
1735
  }
1687
1736
  }
1688
1737
  return suggestions;
@@ -4044,6 +4093,117 @@ Valid properties: EntityName, ExtraFilter, Fields, OrderBy, MaxRows, StartRow, R
4044
4093
  code: `${propName}: ...`
4045
4094
  });
4046
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
+ }
4047
4207
  }
4048
4208
  }
4049
4209
  // Check that EntityName is present (required property)
@@ -4148,6 +4308,111 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4148
4308
  code: `${propName}: ...`
4149
4309
  });
4150
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
+ }
4151
4416
  }
4152
4417
  }
4153
4418
  // Check that at least one required property is present
@@ -4684,8 +4949,10 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4684
4949
  test: (ast, componentName, componentSpec) => {
4685
4950
  const violations = [];
4686
4951
  const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
4687
- // Build set of allowed props: standard props + componentSpec properties
4688
- 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]);
4689
4956
  // Add props from componentSpec.properties if they exist
4690
4957
  if (componentSpec?.properties) {
4691
4958
  for (const prop of componentSpec.properties) {
@@ -4720,7 +4987,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4720
4987
  severity: 'critical',
4721
4988
  line: path.node.loc?.start.line || 0,
4722
4989
  column: path.node.loc?.start.column || 0,
4723
- 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.`
4724
4991
  });
4725
4992
  }
4726
4993
  }
@@ -4753,7 +5020,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4753
5020
  severity: 'critical',
4754
5021
  line: path.node.loc?.start.line || 0,
4755
5022
  column: path.node.loc?.start.column || 0,
4756
- 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.`
4757
5024
  });
4758
5025
  }
4759
5026
  }
@@ -4764,6 +5031,228 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4764
5031
  return violations;
4765
5032
  }
4766
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} ... />`
5136
+ });
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
+ }
5248
+ }
5249
+ }
5250
+ }
5251
+ }
5252
+ });
5253
+ return violations;
5254
+ }
5255
+ },
4767
5256
  {
4768
5257
  name: 'invalid-components-destructuring',
4769
5258
  appliesTo: 'all',
@@ -6903,10 +7392,10 @@ Correct pattern:
6903
7392
  if (!isUsed) {
6904
7393
  violations.push({
6905
7394
  rule: 'unused-component-dependencies',
6906
- severity: 'high',
7395
+ severity: 'low',
6907
7396
  line: 1,
6908
7397
  column: 0,
6909
- message: `Component dependency "${depName}" is declared but never used. This likely means missing functionality.`,
7398
+ message: `Component dependency "${depName}" is declared but never used. Consider removing it if not needed.`,
6910
7399
  code: `Expected usage: <${depName} /> or <components.${depName} />`
6911
7400
  });
6912
7401
  }
@@ -7139,6 +7628,439 @@ Correct pattern:
7139
7628
  }
7140
7629
  return violations;
7141
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
+ }
7142
8064
  }
7143
8065
  ];
7144
8066
  //# sourceMappingURL=component-linter.js.map