@memberjunction/react-test-harness 2.119.0 → 2.121.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.
@@ -4657,9 +4657,57 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4657
4657
  }
4658
4658
  }
4659
4659
  }
4660
- // Skip if no Parameters property
4661
- if (!parametersNode)
4660
+ // IMPORTANT: Validate query name existence FIRST, before checking Parameters
4661
+ // This ensures we catch missing queries even when no Parameters are provided
4662
+ if (queryName && componentSpec?.dataRequirements?.queries) {
4663
+ const queryExists = componentSpec.dataRequirements.queries.some(q => q.name === queryName);
4664
+ if (!queryExists) {
4665
+ const availableQueries = componentSpec.dataRequirements.queries.map(q => q.name).join(', ');
4666
+ violations.push({
4667
+ rule: 'runquery-parameters-validation',
4668
+ severity: 'high',
4669
+ line: path.node.loc?.start.line || 0,
4670
+ column: path.node.loc?.start.column || 0,
4671
+ message: `Query '${queryName}' not found in component spec. Available queries: ${availableQueries || 'none'}`,
4672
+ code: `QueryName: '${componentSpec.dataRequirements.queries[0]?.name || 'QueryNameFromSpec'}'`
4673
+ });
4674
+ }
4675
+ }
4676
+ // Check if query requires parameters but Parameters property is missing
4677
+ if (!parametersNode) {
4678
+ // Find the query spec to check if it has required parameters
4679
+ let specQuery;
4680
+ if (componentSpec?.dataRequirements?.queries && queryName) {
4681
+ specQuery = componentSpec.dataRequirements.queries.find(q => q.name === queryName);
4682
+ }
4683
+ if (specQuery?.parameters && specQuery.parameters.length > 0) {
4684
+ // Check if any parameters are required
4685
+ // Note: isRequired field is being added to ComponentQueryParameterValue type
4686
+ const requiredParams = specQuery.parameters.filter(p => {
4687
+ // Check for explicit isRequired flag (when available)
4688
+ const hasRequiredFlag = p.isRequired === true || p.isRequired === '1';
4689
+ // Or infer required if value is '@runtime' (runtime parameters should be provided)
4690
+ const isRuntimeParam = p.value === '@runtime';
4691
+ return hasRequiredFlag || isRuntimeParam;
4692
+ });
4693
+ if (requiredParams.length > 0) {
4694
+ const paramNames = requiredParams.map(p => p.name).join(', ');
4695
+ const exampleParams = requiredParams
4696
+ .map(p => ` ${p.name}: ${p.testValue ? `'${p.testValue}'` : "'value'"}`)
4697
+ .join(',\n');
4698
+ violations.push({
4699
+ rule: 'runquery-parameters-validation',
4700
+ severity: 'high',
4701
+ line: path.node.loc?.start.line || 0,
4702
+ column: path.node.loc?.start.column || 0,
4703
+ message: `Query '${queryName}' requires parameters but RunQuery call is missing 'Parameters' property. Required: ${paramNames}`,
4704
+ code: `Parameters: {\n${exampleParams}\n}`
4705
+ });
4706
+ }
4707
+ }
4708
+ // Skip further parameter validation since there's no Parameters property
4662
4709
  return;
4710
+ }
4663
4711
  // Find the query in componentSpec if available
4664
4712
  let specQuery;
4665
4713
  if (componentSpec?.dataRequirements?.queries && queryName) {
@@ -4851,21 +4899,8 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4851
4899
  code: fixCode
4852
4900
  });
4853
4901
  }
4854
- // Additional check: Validate against spec queries list
4855
- if (queryName && componentSpec?.dataRequirements?.queries) {
4856
- const queryExists = componentSpec.dataRequirements.queries.some(q => q.name === queryName);
4857
- if (!queryExists) {
4858
- const availableQueries = componentSpec.dataRequirements.queries.map(q => q.name).join(', ');
4859
- violations.push({
4860
- rule: 'runquery-parameters-validation',
4861
- severity: 'high',
4862
- line: path.node.loc?.start.line || 0,
4863
- column: path.node.loc?.start.column || 0,
4864
- message: `Query '${queryName}' not found in component spec. Available queries: ${availableQueries || 'none'}`,
4865
- code: `QueryName: '${componentSpec.dataRequirements.queries[0]?.name || 'QueryNameFromSpec'}'`
4866
- });
4867
- }
4868
- }
4902
+ // Note: Query name validation happens earlier (before Parameters check)
4903
+ // to ensure we catch missing queries even when no Parameters are provided
4869
4904
  }
4870
4905
  }
4871
4906
  });
@@ -5379,228 +5414,6 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5379
5414
  return violations;
5380
5415
  }
5381
5416
  },
5382
- {
5383
- name: 'validate-dependency-props',
5384
- appliesTo: 'all',
5385
- test: (ast, componentName, componentSpec) => {
5386
- const violations = [];
5387
- // Build a map of dependency components to their specs
5388
- const dependencySpecs = new Map();
5389
- // Process embedded dependencies
5390
- if (componentSpec?.dependencies && Array.isArray(componentSpec.dependencies)) {
5391
- for (const dep of componentSpec.dependencies) {
5392
- if (dep && dep.name) {
5393
- if (dep.location === 'registry') {
5394
- const match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace, dep.registry);
5395
- if (!match) {
5396
- // the specified registry component was not found, we can't lint for it, but we should put a warning
5397
- console.warn('Dependency component not found in registry', dep);
5398
- }
5399
- else {
5400
- dependencySpecs.set(dep.name, match.spec);
5401
- }
5402
- }
5403
- else {
5404
- // Embedded dependencies have their spec inline
5405
- dependencySpecs.set(dep.name, dep);
5406
- }
5407
- }
5408
- else {
5409
- // we have an invalid dep in the spec, not a fatal error but we should log this
5410
- console.warn(`Invalid dependency in component spec`, dep);
5411
- }
5412
- }
5413
- }
5414
- // For registry dependencies, we'd need ComponentMetadataEngine
5415
- // But since this is a static lint check, we'll focus on embedded deps
5416
- // Registry components would need async loading which doesn't fit the current sync pattern
5417
- // Now traverse JSX to find component usage
5418
- (0, traverse_1.default)(ast, {
5419
- JSXElement(path) {
5420
- const openingElement = path.node.openingElement;
5421
- // Check if this is one of our dependency components
5422
- if (t.isJSXIdentifier(openingElement.name)) {
5423
- const componentName = openingElement.name.name;
5424
- const depSpec = dependencySpecs.get(componentName);
5425
- if (depSpec) {
5426
- // Collect props being passed
5427
- const passedProps = new Set();
5428
- const passedPropNodes = new Map();
5429
- for (const attr of openingElement.attributes) {
5430
- if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
5431
- const propName = attr.name.name;
5432
- passedProps.add(propName);
5433
- passedPropNodes.set(propName, attr);
5434
- }
5435
- }
5436
- // Check required custom props
5437
- if (depSpec.properties && Array.isArray(depSpec.properties)) {
5438
- const requiredProps = [];
5439
- const optionalProps = [];
5440
- for (const prop of depSpec.properties) {
5441
- if (prop && prop.name && typeof prop.name === 'string') {
5442
- if (prop.required === true) {
5443
- requiredProps.push(prop.name);
5444
- }
5445
- else {
5446
- optionalProps.push(prop.name);
5447
- }
5448
- }
5449
- }
5450
- // Check for missing required props
5451
- const missingRequired = requiredProps.filter(prop => {
5452
- // Special handling for 'children' prop
5453
- if (prop === 'children') {
5454
- // Check if JSX element has children nodes
5455
- const hasChildren = path.node.children && path.node.children.length > 0 &&
5456
- path.node.children.some(child => !t.isJSXText(child) || (t.isJSXText(child) && child.value.trim() !== ''));
5457
- return !passedProps.has(prop) && !hasChildren;
5458
- }
5459
- return !passedProps.has(prop);
5460
- });
5461
- // Separate children warnings from other critical props
5462
- const missingChildren = missingRequired.filter(prop => prop === 'children');
5463
- const missingOtherProps = missingRequired.filter(prop => prop !== 'children');
5464
- // Critical violation for non-children required props
5465
- if (missingOtherProps.length > 0) {
5466
- violations.push({
5467
- rule: 'validate-dependency-props',
5468
- severity: 'critical',
5469
- line: openingElement.loc?.start.line || 0,
5470
- column: openingElement.loc?.start.column || 0,
5471
- message: `Dependency component "${componentName}" is missing required props: ${missingOtherProps.join(', ')}. These props are marked as required in the component's specification.`,
5472
- code: `<${componentName} ... />`
5473
- });
5474
- }
5475
- // Medium severity warning for missing children when required
5476
- if (missingChildren.length > 0) {
5477
- violations.push({
5478
- rule: 'validate-dependency-props',
5479
- severity: 'medium',
5480
- line: openingElement.loc?.start.line || 0,
5481
- column: openingElement.loc?.start.column || 0,
5482
- message: `Component "${componentName}" expects children but none were provided. The 'children' prop is marked as required in the component's specification.`,
5483
- code: `<${componentName} ... />`
5484
- });
5485
- }
5486
- // Validate prop types for passed props
5487
- for (const [propName, attrNode] of passedPropNodes) {
5488
- const propSpec = depSpec.properties.find(p => p.name === propName);
5489
- if (propSpec && propSpec.type) {
5490
- const value = attrNode.value;
5491
- // Type validation based on prop spec type
5492
- if (propSpec.type === 'string') {
5493
- // Check if value could be a string
5494
- if (value && t.isJSXExpressionContainer(value)) {
5495
- const expr = value.expression;
5496
- // Check for obvious non-string types
5497
- if (t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) ||
5498
- t.isArrayExpression(expr) || (t.isObjectExpression(expr) && !t.isTemplateLiteral(expr))) {
5499
- violations.push({
5500
- rule: 'validate-dependency-props',
5501
- severity: 'high',
5502
- line: attrNode.loc?.start.line || 0,
5503
- column: attrNode.loc?.start.column || 0,
5504
- message: `Prop "${propName}" on component "${componentName}" expects type "string" but received a different type.`,
5505
- code: `${propName}={...}`
5506
- });
5507
- }
5508
- }
5509
- }
5510
- else if (propSpec.type === 'number') {
5511
- // Check if value could be a number
5512
- if (value && t.isJSXExpressionContainer(value)) {
5513
- const expr = value.expression;
5514
- if (t.isStringLiteral(expr) || t.isBooleanLiteral(expr) ||
5515
- t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
5516
- violations.push({
5517
- rule: 'validate-dependency-props',
5518
- severity: 'high',
5519
- line: attrNode.loc?.start.line || 0,
5520
- column: attrNode.loc?.start.column || 0,
5521
- message: `Prop "${propName}" on component "${componentName}" expects type "number" but received a different type.`,
5522
- code: `${propName}={...}`
5523
- });
5524
- }
5525
- }
5526
- }
5527
- else if (propSpec.type === 'boolean') {
5528
- // Check if value could be a boolean
5529
- if (value && t.isJSXExpressionContainer(value)) {
5530
- const expr = value.expression;
5531
- if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
5532
- t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
5533
- violations.push({
5534
- rule: 'validate-dependency-props',
5535
- severity: 'high',
5536
- line: attrNode.loc?.start.line || 0,
5537
- column: attrNode.loc?.start.column || 0,
5538
- message: `Prop "${propName}" on component "${componentName}" expects type "boolean" but received a different type.`,
5539
- code: `${propName}={...}`
5540
- });
5541
- }
5542
- }
5543
- }
5544
- else if (propSpec.type === 'array') {
5545
- // Check if value could be an array
5546
- if (value && t.isJSXExpressionContainer(value)) {
5547
- const expr = value.expression;
5548
- if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
5549
- t.isBooleanLiteral(expr) || (t.isObjectExpression(expr) && !t.isArrayExpression(expr))) {
5550
- violations.push({
5551
- rule: 'validate-dependency-props',
5552
- severity: 'high',
5553
- line: attrNode.loc?.start.line || 0,
5554
- column: attrNode.loc?.start.column || 0,
5555
- message: `Prop "${propName}" on component "${componentName}" expects type "array" but received a different type.`,
5556
- code: `${propName}={...}`
5557
- });
5558
- }
5559
- }
5560
- }
5561
- else if (propSpec.type === 'object') {
5562
- // Check if value could be an object
5563
- if (value && t.isJSXExpressionContainer(value)) {
5564
- const expr = value.expression;
5565
- if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
5566
- t.isBooleanLiteral(expr) || t.isArrayExpression(expr)) {
5567
- violations.push({
5568
- rule: 'validate-dependency-props',
5569
- severity: 'high',
5570
- line: attrNode.loc?.start.line || 0,
5571
- column: attrNode.loc?.start.column || 0,
5572
- message: `Prop "${propName}" on component "${componentName}" expects type "object" but received a different type.`,
5573
- code: `${propName}={...}`
5574
- });
5575
- }
5576
- }
5577
- }
5578
- }
5579
- }
5580
- // Check for unknown props (props not in the spec)
5581
- const specPropNames = new Set(depSpec.properties.map(p => p.name).filter(Boolean));
5582
- const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
5583
- const reactSpecialProps = new Set(['children']);
5584
- for (const passedProp of passedProps) {
5585
- if (!specPropNames.has(passedProp) && !standardProps.has(passedProp) && !reactSpecialProps.has(passedProp)) {
5586
- violations.push({
5587
- rule: 'validate-dependency-props',
5588
- severity: 'medium',
5589
- line: passedPropNodes.get(passedProp)?.loc?.start.line || 0,
5590
- column: passedPropNodes.get(passedProp)?.loc?.start.column || 0,
5591
- 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'}.`,
5592
- code: `${passedProp}={...}`
5593
- });
5594
- }
5595
- }
5596
- }
5597
- }
5598
- }
5599
- }
5600
- });
5601
- return violations;
5602
- }
5603
- },
5604
5417
  {
5605
5418
  name: 'unsafe-array-operations',
5606
5419
  appliesTo: 'all',
@@ -6269,6 +6082,39 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
6269
6082
  'response': 'Results',
6270
6083
  'Response': 'Results'
6271
6084
  };
6085
+ // Helper function to validate property access and create violation
6086
+ const validatePropertyAccess = (objName, propName, isFromRunQuery, isFromRunView, line, column, code) => {
6087
+ if (!isFromRunQuery && !isFromRunView)
6088
+ return;
6089
+ const isValidQueryProp = validRunQueryResultProps.has(propName);
6090
+ const isValidViewProp = validRunViewResultProps.has(propName);
6091
+ if (isFromRunQuery && !isValidQueryProp) {
6092
+ const suggestion = incorrectToCorrectMap[propName];
6093
+ violations.push({
6094
+ rule: 'runquery-result-invalid-property',
6095
+ severity: 'critical',
6096
+ line,
6097
+ column,
6098
+ message: suggestion
6099
+ ? `RunQuery results don't have a ".${propName}" property. Use ".${suggestion}" instead. Change "${objName}.${propName}" to "${objName}.${suggestion}"`
6100
+ : `Invalid property "${propName}" on RunQuery result. Valid properties are: ${Array.from(validRunQueryResultProps).join(', ')}`,
6101
+ code
6102
+ });
6103
+ }
6104
+ else if (isFromRunView && !isValidViewProp) {
6105
+ const suggestion = incorrectToCorrectMap[propName];
6106
+ violations.push({
6107
+ rule: 'runview-result-invalid-property',
6108
+ severity: 'critical',
6109
+ line,
6110
+ column,
6111
+ message: suggestion
6112
+ ? `RunView results don't have a ".${propName}" property. Use ".${suggestion}" instead. Change "${objName}.${propName}" to "${objName}.${suggestion}"`
6113
+ : `Invalid property "${propName}" on RunView result. Valid properties are: ${Array.from(validRunViewResultProps).join(', ')}`,
6114
+ code
6115
+ });
6116
+ }
6117
+ };
6272
6118
  (0, traverse_1.default)(ast, {
6273
6119
  MemberExpression(path) {
6274
6120
  // Check if this is accessing a property on a variable that looks like a query/view result
@@ -6280,75 +6126,87 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
6280
6126
  ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
6281
6127
  const isFromRunView = path.scope.hasBinding(objName) &&
6282
6128
  ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
6283
- // Only validate if we're CERTAIN it's from RunQuery or RunView
6284
- if (isFromRunQuery || isFromRunView) {
6285
- // WHITELIST APPROACH: Check if the property is valid for the result type
6286
- const isValidQueryProp = validRunQueryResultProps.has(propName);
6287
- const isValidViewProp = validRunViewResultProps.has(propName);
6288
- // If it's specifically from RunQuery or RunView, be more specific
6289
- if (isFromRunQuery && !isValidQueryProp) {
6290
- // Property is not valid for RunQueryResult
6291
- const suggestion = incorrectToCorrectMap[propName];
6292
- if (suggestion) {
6293
- violations.push({
6294
- rule: 'runquery-result-invalid-property',
6295
- severity: 'critical',
6296
- line: path.node.loc?.start.line || 0,
6297
- column: path.node.loc?.start.column || 0,
6298
- message: `RunQuery results don't have a ".${propName}" property. Use ".${suggestion}" instead. Change "${objName}.${propName}" to "${objName}.${suggestion}"`,
6299
- code: `${objName}.${propName}`
6300
- });
6301
- }
6302
- else {
6303
- violations.push({
6304
- rule: 'runquery-result-invalid-property',
6305
- severity: 'critical',
6306
- line: path.node.loc?.start.line || 0,
6307
- column: path.node.loc?.start.column || 0,
6308
- message: `Invalid property "${propName}" on RunQuery result. Valid properties are: ${Array.from(validRunQueryResultProps).join(', ')}`,
6309
- code: `${objName}.${propName}`
6310
- });
6311
- }
6312
- }
6313
- else if (isFromRunView && !isValidViewProp) {
6314
- // Property is not valid for RunViewResult
6315
- const suggestion = incorrectToCorrectMap[propName];
6316
- if (suggestion) {
6317
- violations.push({
6318
- rule: 'runview-result-invalid-property',
6319
- severity: 'critical',
6320
- line: path.node.loc?.start.line || 0,
6321
- column: path.node.loc?.start.column || 0,
6322
- message: `RunView results don't have a ".${propName}" property. Use ".${suggestion}" instead. Change "${objName}.${propName}" to "${objName}.${suggestion}"`,
6323
- code: `${objName}.${propName}`
6324
- });
6325
- }
6326
- else {
6327
- violations.push({
6328
- rule: 'runview-result-invalid-property',
6329
- severity: 'critical',
6330
- line: path.node.loc?.start.line || 0,
6331
- column: path.node.loc?.start.column || 0,
6332
- message: `Invalid property "${propName}" on RunView result. Valid properties are: ${Array.from(validRunViewResultProps).join(', ')}`,
6333
- code: `${objName}.${propName}`
6129
+ // Use shared validation logic
6130
+ validatePropertyAccess(objName, propName, isFromRunQuery, isFromRunView, path.node.loc?.start.line || 0, path.node.loc?.start.column || 0, `${objName}.${propName}`);
6131
+ // Check for nested incorrect access like result.data.entities or result.Data.entities
6132
+ if ((isFromRunQuery || isFromRunView) &&
6133
+ t.isMemberExpression(path.parent) &&
6134
+ t.isIdentifier(path.parent.property) &&
6135
+ (propName === 'data' || propName === 'Data')) {
6136
+ const nestedProp = path.parent.property.name;
6137
+ violations.push({
6138
+ rule: 'runquery-runview-result-structure',
6139
+ severity: 'critical',
6140
+ line: path.parent.loc?.start.line || 0,
6141
+ column: path.parent.loc?.start.column || 0,
6142
+ message: `Incorrect nested property access "${objName}.${propName}.${nestedProp}". RunQuery/RunView results use ".Results" directly for the data array. Change to "${objName}.Results"`,
6143
+ code: `${objName}.${propName}.${nestedProp}`
6144
+ });
6145
+ }
6146
+ }
6147
+ },
6148
+ // NEW: Handle optional chaining (result?.records, result?.Rows, etc.)
6149
+ OptionalMemberExpression(path) {
6150
+ if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
6151
+ const objName = path.node.object.name;
6152
+ const propName = path.node.property.name;
6153
+ // Only check if we can definitively trace this to RunQuery or RunView
6154
+ const isFromRunQuery = path.scope.hasBinding(objName) &&
6155
+ ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
6156
+ const isFromRunView = path.scope.hasBinding(objName) &&
6157
+ ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
6158
+ // Use shared validation logic
6159
+ validatePropertyAccess(objName, propName, isFromRunQuery, isFromRunView, path.node.loc?.start.line || 0, path.node.loc?.start.column || 0, `${objName}?.${propName}`);
6160
+ }
6161
+ },
6162
+ // NEW: Detect weak fallback patterns like result?.records ?? result?.Rows ?? []
6163
+ LogicalExpression(path) {
6164
+ if (path.node.operator !== '??')
6165
+ return;
6166
+ // Collect all invalid property accesses in the chain
6167
+ const invalidAccesses = [];
6168
+ const checkNode = (node) => {
6169
+ if (t.isOptionalMemberExpression(node) &&
6170
+ t.isIdentifier(node.object) &&
6171
+ t.isIdentifier(node.property)) {
6172
+ const objName = node.object.name;
6173
+ const propName = node.property.name;
6174
+ // Check if this is from RunQuery/RunView
6175
+ const isFromRunQuery = path.scope.hasBinding(objName) &&
6176
+ ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
6177
+ const isFromRunView = path.scope.hasBinding(objName) &&
6178
+ ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
6179
+ if (isFromRunQuery || isFromRunView) {
6180
+ const isValidQueryProp = validRunQueryResultProps.has(propName);
6181
+ const isValidViewProp = validRunViewResultProps.has(propName);
6182
+ if ((isFromRunQuery && !isValidQueryProp) || (isFromRunView && !isValidViewProp)) {
6183
+ invalidAccesses.push({
6184
+ objName,
6185
+ propName,
6186
+ line: node.loc?.start.line || 0
6334
6187
  });
6335
6188
  }
6336
6189
  }
6337
- // Check for nested incorrect access like result.data.entities or result.Data.entities
6338
- if (t.isMemberExpression(path.parent) &&
6339
- t.isIdentifier(path.parent.property) &&
6340
- (propName === 'data' || propName === 'Data')) {
6341
- const nestedProp = path.parent.property.name;
6342
- violations.push({
6343
- rule: 'runquery-runview-result-structure',
6344
- severity: 'critical',
6345
- line: path.parent.loc?.start.line || 0,
6346
- column: path.parent.loc?.start.column || 0,
6347
- message: `Incorrect nested property access "${objName}.${propName}.${nestedProp}". RunQuery/RunView results use ".Results" directly for the data array. Change to "${objName}.Results"`,
6348
- code: `${objName}.${propName}.${nestedProp}`
6349
- });
6350
- }
6351
6190
  }
6191
+ else if (t.isLogicalExpression(node) && node.operator === '??') {
6192
+ // Recursively check chained ?? operators
6193
+ checkNode(node.left);
6194
+ checkNode(node.right);
6195
+ }
6196
+ };
6197
+ checkNode(path.node);
6198
+ // If we found multiple invalid accesses in a chain, report as weak fallback
6199
+ if (invalidAccesses.length >= 2) {
6200
+ const objName = invalidAccesses[0].objName;
6201
+ const props = invalidAccesses.map(a => a.propName).join(', ');
6202
+ violations.push({
6203
+ rule: 'runquery-runview-result-structure',
6204
+ severity: 'critical',
6205
+ line: path.node.loc?.start.line || 0,
6206
+ column: path.node.loc?.start.column || 0,
6207
+ message: `Weak fallback pattern detected: "${objName}?.${invalidAccesses[0].propName} ?? ${objName}?.${invalidAccesses[1].propName} ?? ..." uses multiple INVALID properties (${props}). This masks the real issue. Use "${objName}?.Results ?? []" instead. RunQuery/RunView results have a "Results" property (capital R), not "${props}".`,
6208
+ code: path.toString().substring(0, 100)
6209
+ });
6352
6210
  }
6353
6211
  },
6354
6212
  // Check for destructuring patterns
@@ -6653,20 +6511,38 @@ Correct pattern:
6653
6511
  if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
6654
6512
  return violations;
6655
6513
  }
6656
- // Build a map of dependency components and their expected props
6657
- const dependencyPropsMap = new Map();
6514
+ // Build a map of dependency components to their full specs
6515
+ const dependencySpecs = new Map();
6516
+ // Process all dependencies (embedded and registry)
6658
6517
  for (const dep of componentSpec.dependencies) {
6659
- const requiredProps = dep.properties
6660
- ?.filter(p => p.required)
6661
- ?.map(p => p.name) || [];
6662
- const allProps = dep.properties?.map(p => p.name) || [];
6663
- dependencyPropsMap.set(dep.name, {
6664
- required: requiredProps,
6665
- all: allProps,
6666
- location: dep.location || 'embedded'
6667
- });
6518
+ if (dep && dep.name) {
6519
+ if (dep.location === 'registry') {
6520
+ // Try to load from registry
6521
+ // check if registry is defined; if not, don't pass it to find component
6522
+ let match;
6523
+ if (dep.registry) {
6524
+ match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace, dep.registry);
6525
+ }
6526
+ else {
6527
+ match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace);
6528
+ }
6529
+ if (!match) {
6530
+ console.warn(`Dependency component not found in registry: ${dep.name} (${dep.namespace || 'no namespace'})`);
6531
+ }
6532
+ else {
6533
+ dependencySpecs.set(dep.name, match.spec);
6534
+ }
6535
+ }
6536
+ else {
6537
+ // Embedded dependencies have their spec inline
6538
+ dependencySpecs.set(dep.name, dep);
6539
+ }
6540
+ }
6541
+ else {
6542
+ console.warn(`Invalid dependency in component spec: ${dep?.name || 'unknown'}`);
6543
+ }
6668
6544
  }
6669
- // Helper function to find closest matching prop name
6545
+ // Helper function to find closest matching prop name using Levenshtein distance
6670
6546
  function findClosestMatch(target, candidates) {
6671
6547
  if (candidates.length === 0)
6672
6548
  return null;
@@ -6705,10 +6581,11 @@ Correct pattern:
6705
6581
  }
6706
6582
  // Standard props that are always valid (passed by the runtime)
6707
6583
  const standardProps = new Set([
6708
- 'styles', 'utilities', 'components', 'callbacks',
6584
+ 'utilities', 'styles', 'components', 'callbacks',
6709
6585
  'savedUserSettings', 'onSaveUserSettings'
6710
6586
  ]);
6711
- // Track JSX elements and their props
6587
+ const reactSpecialProps = new Set(['children', 'key', 'ref']);
6588
+ // Traverse JSX to find component usage
6712
6589
  (0, traverse_1.default)(ast, {
6713
6590
  JSXElement(path) {
6714
6591
  const openingElement = path.node.openingElement;
@@ -6718,69 +6595,204 @@ Correct pattern:
6718
6595
  elementName = openingElement.name.name;
6719
6596
  }
6720
6597
  else if (t.isJSXMemberExpression(openingElement.name)) {
6721
- // Handle cases like <MaterialUI.Button>
6722
- return; // Skip member expressions for now
6598
+ // Handle cases like <components.Button> - skip for now
6599
+ return;
6723
6600
  }
6724
- // Check if this is one of our dependencies
6725
- if (dependencyPropsMap.has(elementName)) {
6726
- const { required, all, location } = dependencyPropsMap.get(elementName);
6727
- // Get passed props
6728
- const passedProps = new Set();
6729
- const propLocations = new Map();
6730
- for (const attr of openingElement.attributes) {
6731
- if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
6732
- const propName = attr.name.name;
6733
- passedProps.add(propName);
6734
- propLocations.set(propName, {
6735
- line: attr.loc?.start.line || 0,
6736
- column: attr.loc?.start.column || 0
6737
- });
6601
+ // Check if this is one of our dependency components
6602
+ const depSpec = dependencySpecs.get(elementName);
6603
+ if (!depSpec)
6604
+ return;
6605
+ // Collect passed props
6606
+ const passedProps = new Set();
6607
+ const passedPropNodes = new Map();
6608
+ for (const attr of openingElement.attributes) {
6609
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
6610
+ const propName = attr.name.name;
6611
+ passedProps.add(propName);
6612
+ passedPropNodes.set(propName, attr);
6613
+ }
6614
+ }
6615
+ // Build lists of valid props and events
6616
+ const specPropNames = depSpec.properties?.map(p => p.name).filter(Boolean) || [];
6617
+ const specEventNames = depSpec.events?.map(e => e.name).filter(Boolean) || [];
6618
+ const allValidProps = [...specPropNames, ...specEventNames];
6619
+ // Get required props
6620
+ const requiredProps = [];
6621
+ if (depSpec.properties && Array.isArray(depSpec.properties)) {
6622
+ for (const prop of depSpec.properties) {
6623
+ if (prop && prop.name && prop.required === true) {
6624
+ requiredProps.push(prop.name);
6625
+ }
6626
+ }
6627
+ }
6628
+ // ═══════════════════════════════════════════════════════════════
6629
+ // 1. CHECK MISSING REQUIRED PROPS
6630
+ // ═══════════════════════════════════════════════════════════════
6631
+ const missingRequired = requiredProps.filter(prop => {
6632
+ // Special handling for 'children' prop
6633
+ if (prop === 'children') {
6634
+ // Check if JSX element has children nodes
6635
+ const hasChildren = path.node.children && path.node.children.length > 0 &&
6636
+ path.node.children.some(child => !t.isJSXText(child) || (t.isJSXText(child) && child.value.trim() !== ''));
6637
+ return !passedProps.has(prop) && !hasChildren;
6638
+ }
6639
+ return !passedProps.has(prop) && !standardProps.has(prop);
6640
+ });
6641
+ // Separate children warnings from other critical props
6642
+ const missingChildren = missingRequired.filter(prop => prop === 'children');
6643
+ const missingOtherProps = missingRequired.filter(prop => prop !== 'children');
6644
+ // Critical violation for non-children required props
6645
+ if (missingOtherProps.length > 0) {
6646
+ violations.push({
6647
+ rule: 'dependency-prop-validation',
6648
+ severity: 'critical',
6649
+ line: openingElement.loc?.start.line || 0,
6650
+ column: openingElement.loc?.start.column || 0,
6651
+ message: `Dependency component "${elementName}" is missing required props: ${missingOtherProps.join(', ')}. These props are marked as required in the component's specification.`,
6652
+ code: `<${elementName} ... />`
6653
+ });
6654
+ }
6655
+ // Medium severity warning for missing children when required
6656
+ if (missingChildren.length > 0) {
6657
+ violations.push({
6658
+ rule: 'dependency-prop-validation',
6659
+ severity: 'medium',
6660
+ line: openingElement.loc?.start.line || 0,
6661
+ column: openingElement.loc?.start.column || 0,
6662
+ message: `Component "${elementName}" expects children but none were provided. The 'children' prop is marked as required in the component's specification.`,
6663
+ code: `<${elementName} ... />`
6664
+ });
6665
+ }
6666
+ // ═══════════════════════════════════════════════════════════════
6667
+ // 2. VALIDATE PROP TYPES
6668
+ // ═══════════════════════════════════════════════════════════════
6669
+ if (depSpec.properties && Array.isArray(depSpec.properties)) {
6670
+ for (const [propName, attrNode] of passedPropNodes) {
6671
+ const propSpec = depSpec.properties.find(p => p.name === propName);
6672
+ if (propSpec && propSpec.type) {
6673
+ const value = attrNode.value;
6674
+ // Type validation based on prop spec type
6675
+ if (propSpec.type === 'string') {
6676
+ // Check if value could be a string
6677
+ if (value && t.isJSXExpressionContainer(value)) {
6678
+ const expr = value.expression;
6679
+ // Check for obvious non-string types
6680
+ if (t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) ||
6681
+ t.isArrayExpression(expr) || (t.isObjectExpression(expr) && !t.isTemplateLiteral(expr))) {
6682
+ violations.push({
6683
+ rule: 'dependency-prop-validation',
6684
+ severity: 'high',
6685
+ line: attrNode.loc?.start.line || 0,
6686
+ column: attrNode.loc?.start.column || 0,
6687
+ message: `Prop "${propName}" on component "${elementName}" expects type "string" but received a different type.`,
6688
+ code: `${propName}={...}`
6689
+ });
6690
+ }
6691
+ }
6692
+ }
6693
+ else if (propSpec.type === 'number') {
6694
+ // Check if value could be a number
6695
+ if (value && t.isJSXExpressionContainer(value)) {
6696
+ const expr = value.expression;
6697
+ if (t.isStringLiteral(expr) || t.isBooleanLiteral(expr) ||
6698
+ t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
6699
+ violations.push({
6700
+ rule: 'dependency-prop-validation',
6701
+ severity: 'high',
6702
+ line: attrNode.loc?.start.line || 0,
6703
+ column: attrNode.loc?.start.column || 0,
6704
+ message: `Prop "${propName}" on component "${elementName}" expects type "number" but received a different type.`,
6705
+ code: `${propName}={...}`
6706
+ });
6707
+ }
6708
+ }
6709
+ }
6710
+ else if (propSpec.type === 'boolean') {
6711
+ // Check if value could be a boolean
6712
+ if (value && t.isJSXExpressionContainer(value)) {
6713
+ const expr = value.expression;
6714
+ if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
6715
+ t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
6716
+ violations.push({
6717
+ rule: 'dependency-prop-validation',
6718
+ severity: 'high',
6719
+ line: attrNode.loc?.start.line || 0,
6720
+ column: attrNode.loc?.start.column || 0,
6721
+ message: `Prop "${propName}" on component "${elementName}" expects type "boolean" but received a different type.`,
6722
+ code: `${propName}={...}`
6723
+ });
6724
+ }
6725
+ }
6726
+ }
6727
+ else if (propSpec.type === 'array' || propSpec.type.startsWith('Array<')) {
6728
+ // Check if value could be an array
6729
+ if (value && t.isJSXExpressionContainer(value)) {
6730
+ const expr = value.expression;
6731
+ if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
6732
+ t.isBooleanLiteral(expr) || (t.isObjectExpression(expr) && !t.isArrayExpression(expr))) {
6733
+ violations.push({
6734
+ rule: 'dependency-prop-validation',
6735
+ severity: 'high',
6736
+ line: attrNode.loc?.start.line || 0,
6737
+ column: attrNode.loc?.start.column || 0,
6738
+ message: `Prop "${propName}" on component "${elementName}" expects type "array" but received a different type.`,
6739
+ code: `${propName}={...}`
6740
+ });
6741
+ }
6742
+ }
6743
+ }
6744
+ else if (propSpec.type === 'object') {
6745
+ // Check if value could be an object
6746
+ if (value && t.isJSXExpressionContainer(value)) {
6747
+ const expr = value.expression;
6748
+ if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
6749
+ t.isBooleanLiteral(expr) || t.isArrayExpression(expr)) {
6750
+ violations.push({
6751
+ rule: 'dependency-prop-validation',
6752
+ severity: 'high',
6753
+ line: attrNode.loc?.start.line || 0,
6754
+ column: attrNode.loc?.start.column || 0,
6755
+ message: `Prop "${propName}" on component "${elementName}" expects type "object" but received a different type.`,
6756
+ code: `${propName}={...}`
6757
+ });
6758
+ }
6759
+ }
6760
+ }
6738
6761
  }
6739
6762
  }
6740
- // Check for missing required props
6741
- for (const requiredProp of required) {
6742
- if (!passedProps.has(requiredProp) && !standardProps.has(requiredProp)) {
6763
+ }
6764
+ // ═══════════════════════════════════════════════════════════════
6765
+ // 3. CHECK UNKNOWN PROPS (with Levenshtein suggestions)
6766
+ // ═══════════════════════════════════════════════════════════════
6767
+ for (const passedProp of passedProps) {
6768
+ // Skip standard props and React special props
6769
+ if (standardProps.has(passedProp) || reactSpecialProps.has(passedProp)) {
6770
+ continue;
6771
+ }
6772
+ // Check if prop is valid (in properties or events)
6773
+ if (!allValidProps.includes(passedProp)) {
6774
+ // Try to find a close match using Levenshtein distance
6775
+ const suggestion = findClosestMatch(passedProp, allValidProps);
6776
+ const loc = passedPropNodes.get(passedProp);
6777
+ if (suggestion) {
6743
6778
  violations.push({
6744
6779
  rule: 'dependency-prop-validation',
6745
- severity: 'critical',
6746
- line: openingElement.loc?.start.line || 0,
6747
- column: openingElement.loc?.start.column || 0,
6748
- message: `Missing required prop '${requiredProp}' for dependency component '${elementName}'`,
6749
- code: `<${elementName} ... />`
6780
+ severity: 'high',
6781
+ line: loc?.loc?.start.line || openingElement.loc?.start.line || 0,
6782
+ column: loc?.loc?.start.column || openingElement.loc?.start.column || 0,
6783
+ message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Did you mean '${suggestion}'?`,
6784
+ code: `${passedProp}={...}`
6750
6785
  });
6751
6786
  }
6752
- }
6753
- // Check for unknown props (potential typos)
6754
- for (const passedProp of passedProps) {
6755
- // Skip standard props and spread operators
6756
- if (standardProps.has(passedProp) || passedProp === 'key' || passedProp === 'ref') {
6757
- continue;
6758
- }
6759
- if (!all.includes(passedProp)) {
6760
- // Try to find a close match
6761
- const suggestion = findClosestMatch(passedProp, all);
6762
- if (suggestion) {
6763
- const loc = propLocations.get(passedProp);
6764
- violations.push({
6765
- rule: 'dependency-prop-validation',
6766
- severity: 'high',
6767
- line: loc?.line || openingElement.loc?.start.line || 0,
6768
- column: loc?.column || openingElement.loc?.start.column || 0,
6769
- message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Did you mean '${suggestion}'?`,
6770
- code: `${passedProp}={...}`
6771
- });
6772
- }
6773
- else {
6774
- const loc = propLocations.get(passedProp);
6775
- violations.push({
6776
- rule: 'dependency-prop-validation',
6777
- severity: 'medium',
6778
- line: loc?.line || openingElement.loc?.start.line || 0,
6779
- column: loc?.column || openingElement.loc?.start.column || 0,
6780
- message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Expected props: ${all.join(', ')}`,
6781
- code: `${passedProp}={...}`
6782
- });
6783
- }
6787
+ else {
6788
+ violations.push({
6789
+ rule: 'dependency-prop-validation',
6790
+ severity: 'medium',
6791
+ line: loc?.loc?.start.line || openingElement.loc?.start.line || 0,
6792
+ column: loc?.loc?.start.column || openingElement.loc?.start.column || 0,
6793
+ message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Expected props and events: ${allValidProps.join(', ') || 'none'}.`,
6794
+ code: `${passedProp}={...}`
6795
+ });
6784
6796
  }
6785
6797
  }
6786
6798
  }