@memberjunction/react-test-harness 2.92.0 → 2.94.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.
@@ -1,5 +1,6 @@
1
1
  import { ComponentSpec } from '@memberjunction/interactive-component-types';
2
2
  import type { UserInfo } from '@memberjunction/core';
3
+ import { ComponentExecutionOptions } from './component-runner';
3
4
  export interface LintResult {
4
5
  success: boolean;
5
6
  violations: Violation[];
@@ -29,7 +30,7 @@ export declare class ComponentLinter {
29
30
  private static containsReturn;
30
31
  private static isVariableFromRunQueryOrView;
31
32
  private static universalComponentRules;
32
- static lintComponent(code: string, componentName: string, componentSpec?: ComponentSpec, isRootComponent?: boolean, contextUser?: UserInfo, debugMode?: boolean): Promise<LintResult>;
33
+ static lintComponent(code: string, componentName: string, componentSpec?: ComponentSpec, isRootComponent?: boolean, contextUser?: UserInfo, debugMode?: boolean, options?: ComponentExecutionOptions): Promise<LintResult>;
33
34
  private static validateDataRequirements;
34
35
  private static getFunctionName;
35
36
  private static deduplicateViolations;
@@ -1 +1 @@
1
- {"version":3,"file":"component-linter.d.ts","sourceRoot":"","sources":["../../src/lib/component-linter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAiC,MAAM,6CAA6C,CAAC;AAG3G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGrD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+ED,qBAAa,eAAe;IAE1B,OAAO,CAAC,MAAM,CAAC,cAAc;IAoB7B,OAAO,CAAC,MAAM,CAAC,4BAA4B;IA2C3C,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAw6HpC;WAEkB,aAAa,CAC/B,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,aAAa,EAC7B,eAAe,CAAC,EAAE,OAAO,EACzB,WAAW,CAAC,EAAE,QAAQ,EACtB,SAAS,CAAC,EAAE,OAAO,GAClB,OAAO,CAAC,UAAU,CAAC;IA0GtB,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAoXvC,OAAO,CAAC,MAAM,CAAC,eAAe;IA2B9B,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAyBpC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAw/BrC;;OAEG;mBACkB,qBAAqB;IA2H1C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAiEzC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA8ClC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA8GpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAoElC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;CAuFzC"}
1
+ {"version":3,"file":"component-linter.d.ts","sourceRoot":"","sources":["../../src/lib/component-linter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAiC,MAAM,6CAA6C,CAAC;AAG3G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+ED,qBAAa,eAAe;IAE1B,OAAO,CAAC,MAAM,CAAC,cAAc;IAoB7B,OAAO,CAAC,MAAM,CAAC,4BAA4B;IA2C3C,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAm7IpC;WAEkB,aAAa,CAC/B,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,aAAa,EAC7B,eAAe,CAAC,EAAE,OAAO,EACzB,WAAW,CAAC,EAAE,QAAQ,EACtB,SAAS,CAAC,EAAE,OAAO,EACnB,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,UAAU,CAAC;IA0GtB,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAoXvC,OAAO,CAAC,MAAM,CAAC,eAAe;IA2B9B,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAyBpC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAw/BrC;;OAEG;mBACkB,qBAAqB;IA2H1C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAiEzC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA8ClC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA8GpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAoElC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;CAuFzC"}
@@ -152,7 +152,7 @@ class ComponentLinter {
152
152
  }
153
153
  return isFromMethod;
154
154
  }
155
- static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode) {
155
+ static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode, options) {
156
156
  try {
157
157
  const ast = parser.parse(code, {
158
158
  sourceType: 'module',
@@ -173,7 +173,7 @@ class ComponentLinter {
173
173
  const violations = [];
174
174
  // Run each rule
175
175
  for (const rule of rules) {
176
- const ruleViolations = rule.test(ast, componentName, componentSpec);
176
+ const ruleViolations = rule.test(ast, componentName, componentSpec, options);
177
177
  violations.push(...ruleViolations);
178
178
  }
179
179
  // Add data requirements validation if componentSpec is provided
@@ -3649,9 +3649,8 @@ ComponentLinter.universalComponentRules = [
3649
3649
  const violations = [];
3650
3650
  // Valid properties for RunView/RunViews
3651
3651
  const validRunViewProps = new Set([
3652
- 'ViewID', 'ViewName', 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
3653
- 'MaxRows', 'StartRow', 'ResultType', 'UserSearchString', 'ForceAuditLog', 'AuditLogDescription',
3654
- 'ResultType'
3652
+ 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
3653
+ 'MaxRows', 'StartRow', 'ResultType'
3655
3654
  ]);
3656
3655
  // Valid properties for RunQuery
3657
3656
  const validRunQueryProps = new Set([
@@ -3729,20 +3728,11 @@ ComponentLinter.universalComponentRules = [
3729
3728
  }
3730
3729
  // Check each config for invalid properties and required fields
3731
3730
  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;
3731
+ // Check for required properties (must have EntityName)
3736
3732
  let hasEntityName = false;
3737
3733
  for (const prop of config.properties) {
3738
3734
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3739
3735
  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
3736
  if (propName === 'EntityName')
3747
3737
  hasEntityName = true;
3748
3738
  if (!validRunViewProps.has(propName)) {
@@ -3753,6 +3743,18 @@ ComponentLinter.universalComponentRules = [
3753
3743
  message = `${methodName} does not support 'Parameters'. Use 'ExtraFilter' for WHERE clauses.`;
3754
3744
  fix = `Replace 'Parameters' with 'ExtraFilter' and format as SQL WHERE clause`;
3755
3745
  }
3746
+ else if (propName === 'ViewID' || propName === 'ViewName') {
3747
+ message = `${methodName} property '${propName}' is not allowed in components. Use 'EntityName' instead.`;
3748
+ fix = `Replace '${propName}' with 'EntityName' and specify the entity name`;
3749
+ }
3750
+ else if (propName === 'UserSearchString') {
3751
+ message = `${methodName} property 'UserSearchString' is not allowed in components. Use 'ExtraFilter' for filtering.`;
3752
+ fix = `Remove 'UserSearchString' and use 'ExtraFilter' with appropriate WHERE clause`;
3753
+ }
3754
+ else if (propName === 'ForceAuditLog' || propName === 'AuditLogDescription') {
3755
+ message = `${methodName} property '${propName}' is not allowed in components.`;
3756
+ fix = `Remove '${propName}' property`;
3757
+ }
3756
3758
  else if (propName === 'GroupBy') {
3757
3759
  message = `${methodName} does not support 'GroupBy'. Use RunQuery with a pre-defined query for aggregations.`;
3758
3760
  fix = `Remove 'GroupBy' and use RunQuery instead for aggregated data`;
@@ -3772,14 +3774,14 @@ ComponentLinter.universalComponentRules = [
3772
3774
  }
3773
3775
  }
3774
3776
  }
3775
- // Check that at least one required property is present
3776
- if (!hasViewID && !hasViewName && !hasViewEntity && !hasEntityName) {
3777
+ // Check that EntityName is present (required property)
3778
+ if (!hasEntityName) {
3777
3779
  violations.push({
3778
3780
  rule: 'runview-runquery-valid-properties',
3779
3781
  severity: 'critical',
3780
3782
  line: config.loc?.start.line || 0,
3781
3783
  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.`,
3784
+ message: `${methodName} requires 'EntityName' property. Add EntityName to identify what data to retrieve.`,
3783
3785
  code: `${methodName}({ ... })`
3784
3786
  });
3785
3787
  }
@@ -5635,6 +5637,443 @@ ComponentLinter.universalComponentRules = [
5635
5637
  });
5636
5638
  return violations;
5637
5639
  }
5640
+ },
5641
+ {
5642
+ name: 'utilities-valid-properties',
5643
+ appliesTo: 'all',
5644
+ test: (ast, componentName, componentSpec) => {
5645
+ const violations = [];
5646
+ const validProperties = new Set(['rv', 'rq', 'md', 'ai']);
5647
+ (0, traverse_1.default)(ast, {
5648
+ MemberExpression(path) {
5649
+ // Check for utilities.* access
5650
+ if (t.isIdentifier(path.node.object) && path.node.object.name === 'utilities') {
5651
+ if (t.isIdentifier(path.node.property)) {
5652
+ const propName = path.node.property.name;
5653
+ // Check if it's a valid property
5654
+ if (!validProperties.has(propName)) {
5655
+ violations.push({
5656
+ rule: 'utilities-valid-properties',
5657
+ severity: 'critical',
5658
+ line: path.node.loc?.start.line || 0,
5659
+ column: path.node.loc?.start.column || 0,
5660
+ message: `Invalid utilities property '${propName}'. Valid properties are: rv (RunView), rq (RunQuery), md (Metadata), ai (AI Tools)`,
5661
+ code: `utilities.${propName}`
5662
+ });
5663
+ }
5664
+ }
5665
+ }
5666
+ }
5667
+ });
5668
+ return violations;
5669
+ }
5670
+ },
5671
+ {
5672
+ name: 'utilities-runview-methods',
5673
+ appliesTo: 'all',
5674
+ test: (ast, componentName, componentSpec) => {
5675
+ const violations = [];
5676
+ const validMethods = new Set(['RunView', 'RunViews']);
5677
+ (0, traverse_1.default)(ast, {
5678
+ CallExpression(path) {
5679
+ // Check for utilities.rv.* method calls
5680
+ if (t.isMemberExpression(path.node.callee)) {
5681
+ const callee = path.node.callee;
5682
+ // Check if it's utilities.rv.methodName()
5683
+ if (t.isMemberExpression(callee.object) &&
5684
+ t.isIdentifier(callee.object.object) &&
5685
+ callee.object.object.name === 'utilities' &&
5686
+ t.isIdentifier(callee.object.property) &&
5687
+ callee.object.property.name === 'rv' &&
5688
+ t.isIdentifier(callee.property)) {
5689
+ const methodName = callee.property.name;
5690
+ if (!validMethods.has(methodName)) {
5691
+ violations.push({
5692
+ rule: 'utilities-runview-methods',
5693
+ severity: 'critical',
5694
+ line: path.node.loc?.start.line || 0,
5695
+ column: path.node.loc?.start.column || 0,
5696
+ message: `Invalid method '${methodName}' on utilities.rv. Valid methods are: RunView, RunViews`,
5697
+ code: `utilities.rv.${methodName}()`
5698
+ });
5699
+ }
5700
+ }
5701
+ }
5702
+ }
5703
+ });
5704
+ return violations;
5705
+ }
5706
+ },
5707
+ {
5708
+ name: 'utilities-runquery-methods',
5709
+ appliesTo: 'all',
5710
+ test: (ast, componentName, componentSpec) => {
5711
+ const violations = [];
5712
+ const validMethods = new Set(['RunQuery']);
5713
+ (0, traverse_1.default)(ast, {
5714
+ CallExpression(path) {
5715
+ // Check for utilities.rq.* method calls
5716
+ if (t.isMemberExpression(path.node.callee)) {
5717
+ const callee = path.node.callee;
5718
+ // Check if it's utilities.rq.methodName()
5719
+ if (t.isMemberExpression(callee.object) &&
5720
+ t.isIdentifier(callee.object.object) &&
5721
+ callee.object.object.name === 'utilities' &&
5722
+ t.isIdentifier(callee.object.property) &&
5723
+ callee.object.property.name === 'rq' &&
5724
+ t.isIdentifier(callee.property)) {
5725
+ const methodName = callee.property.name;
5726
+ if (!validMethods.has(methodName)) {
5727
+ violations.push({
5728
+ rule: 'utilities-runquery-methods',
5729
+ severity: 'critical',
5730
+ line: path.node.loc?.start.line || 0,
5731
+ column: path.node.loc?.start.column || 0,
5732
+ message: `Invalid method '${methodName}' on utilities.rq. Valid method is: RunQuery`,
5733
+ code: `utilities.rq.${methodName}()`
5734
+ });
5735
+ }
5736
+ }
5737
+ }
5738
+ }
5739
+ });
5740
+ return violations;
5741
+ }
5742
+ },
5743
+ {
5744
+ name: 'utilities-metadata-methods',
5745
+ appliesTo: 'all',
5746
+ test: (ast, componentName, componentSpec) => {
5747
+ const violations = [];
5748
+ const validMethods = new Set(['GetEntityObject']);
5749
+ const validProperties = new Set(['Entities']);
5750
+ (0, traverse_1.default)(ast, {
5751
+ // Check for method calls
5752
+ CallExpression(path) {
5753
+ // Check for utilities.md.* method calls
5754
+ if (t.isMemberExpression(path.node.callee)) {
5755
+ const callee = path.node.callee;
5756
+ // Check if it's utilities.md.methodName()
5757
+ if (t.isMemberExpression(callee.object) &&
5758
+ t.isIdentifier(callee.object.object) &&
5759
+ callee.object.object.name === 'utilities' &&
5760
+ t.isIdentifier(callee.object.property) &&
5761
+ callee.object.property.name === 'md' &&
5762
+ t.isIdentifier(callee.property)) {
5763
+ const methodName = callee.property.name;
5764
+ if (!validMethods.has(methodName)) {
5765
+ violations.push({
5766
+ rule: 'utilities-metadata-methods',
5767
+ severity: 'critical',
5768
+ line: path.node.loc?.start.line || 0,
5769
+ column: path.node.loc?.start.column || 0,
5770
+ message: `Invalid method '${methodName}' on utilities.md. Valid methods are: GetEntityObject. Valid properties are: Entities`,
5771
+ code: `utilities.md.${methodName}()`
5772
+ });
5773
+ }
5774
+ }
5775
+ }
5776
+ },
5777
+ // Check for property access (non-call expressions)
5778
+ MemberExpression(path) {
5779
+ // Skip if this is part of a call expression (handled above)
5780
+ if (t.isCallExpression(path.parent) && path.parent.callee === path.node) {
5781
+ return;
5782
+ }
5783
+ // Check if it's utilities.md.propertyName
5784
+ if (t.isMemberExpression(path.node.object) &&
5785
+ t.isIdentifier(path.node.object.object) &&
5786
+ path.node.object.object.name === 'utilities' &&
5787
+ t.isIdentifier(path.node.object.property) &&
5788
+ path.node.object.property.name === 'md' &&
5789
+ t.isIdentifier(path.node.property)) {
5790
+ const propName = path.node.property.name;
5791
+ // Check if it's accessing a valid property or trying to access an invalid one
5792
+ if (!validProperties.has(propName) && !validMethods.has(propName)) {
5793
+ violations.push({
5794
+ rule: 'utilities-metadata-methods',
5795
+ severity: 'critical',
5796
+ line: path.node.loc?.start.line || 0,
5797
+ column: path.node.loc?.start.column || 0,
5798
+ message: `Invalid property '${propName}' on utilities.md. Valid methods are: GetEntityObject. Valid properties are: Entities`,
5799
+ code: `utilities.md.${propName}`
5800
+ });
5801
+ }
5802
+ }
5803
+ }
5804
+ });
5805
+ return violations;
5806
+ }
5807
+ },
5808
+ {
5809
+ name: 'utilities-ai-methods',
5810
+ appliesTo: 'all',
5811
+ test: (ast, componentName, componentSpec) => {
5812
+ const violations = [];
5813
+ const validMethods = new Set(['ExecutePrompt', 'EmbedText']);
5814
+ const validProperties = new Set(['VectorService']);
5815
+ (0, traverse_1.default)(ast, {
5816
+ // Check for method calls
5817
+ CallExpression(path) {
5818
+ // Check for utilities.ai.* method calls
5819
+ if (t.isMemberExpression(path.node.callee)) {
5820
+ const callee = path.node.callee;
5821
+ // Check if it's utilities.ai.methodName()
5822
+ if (t.isMemberExpression(callee.object) &&
5823
+ t.isIdentifier(callee.object.object) &&
5824
+ callee.object.object.name === 'utilities' &&
5825
+ t.isIdentifier(callee.object.property) &&
5826
+ callee.object.property.name === 'ai' &&
5827
+ t.isIdentifier(callee.property)) {
5828
+ const methodName = callee.property.name;
5829
+ if (!validMethods.has(methodName)) {
5830
+ violations.push({
5831
+ rule: 'utilities-ai-methods',
5832
+ severity: 'critical',
5833
+ line: path.node.loc?.start.line || 0,
5834
+ column: path.node.loc?.start.column || 0,
5835
+ message: `Invalid method '${methodName}' on utilities.ai. Valid methods are: ExecutePrompt, EmbedText. Valid property: VectorService`,
5836
+ code: `utilities.ai.${methodName}()`
5837
+ });
5838
+ }
5839
+ }
5840
+ }
5841
+ },
5842
+ // Check for property access (VectorService)
5843
+ MemberExpression(path) {
5844
+ // Skip if this is part of a call expression (handled above)
5845
+ if (t.isCallExpression(path.parent)) {
5846
+ return;
5847
+ }
5848
+ // Check if it's utilities.ai.propertyName
5849
+ if (t.isMemberExpression(path.node.object) &&
5850
+ t.isIdentifier(path.node.object.object) &&
5851
+ path.node.object.object.name === 'utilities' &&
5852
+ t.isIdentifier(path.node.object.property) &&
5853
+ path.node.object.property.name === 'ai' &&
5854
+ t.isIdentifier(path.node.property)) {
5855
+ const propName = path.node.property.name;
5856
+ // Check if it's a valid property or method (methods might be referenced without calling)
5857
+ if (!validProperties.has(propName) && !validMethods.has(propName)) {
5858
+ violations.push({
5859
+ rule: 'utilities-ai-properties',
5860
+ severity: 'critical',
5861
+ line: path.node.loc?.start.line || 0,
5862
+ column: path.node.loc?.start.column || 0,
5863
+ message: `Invalid property '${propName}' on utilities.ai. Valid methods are: ExecutePrompt, EmbedText. Valid property: VectorService`,
5864
+ code: `utilities.ai.${propName}`
5865
+ });
5866
+ }
5867
+ }
5868
+ }
5869
+ });
5870
+ return violations;
5871
+ }
5872
+ },
5873
+ {
5874
+ name: 'utilities-no-direct-instantiation',
5875
+ appliesTo: 'all',
5876
+ test: (ast, componentName, componentSpec) => {
5877
+ const violations = [];
5878
+ const restrictedClasses = new Map([
5879
+ ['RunView', 'utilities.rv'],
5880
+ ['RunQuery', 'utilities.rq'],
5881
+ ['Metadata', 'utilities.md'],
5882
+ ['SimpleVectorService', 'utilities.ai.VectorService']
5883
+ ]);
5884
+ (0, traverse_1.default)(ast, {
5885
+ NewExpression(path) {
5886
+ // Check if instantiating a restricted class
5887
+ if (t.isIdentifier(path.node.callee)) {
5888
+ const className = path.node.callee.name;
5889
+ if (restrictedClasses.has(className)) {
5890
+ const utilityPath = restrictedClasses.get(className);
5891
+ violations.push({
5892
+ rule: 'utilities-no-direct-instantiation',
5893
+ severity: 'high',
5894
+ line: path.node.loc?.start.line || 0,
5895
+ column: path.node.loc?.start.column || 0,
5896
+ message: `Don't instantiate ${className} directly. Use ${utilityPath} instead which is provided in the component's utilities parameter.`,
5897
+ code: `new ${className}()`
5898
+ });
5899
+ }
5900
+ }
5901
+ }
5902
+ });
5903
+ return violations;
5904
+ }
5905
+ },
5906
+ {
5907
+ name: 'unsafe-formatting-methods',
5908
+ appliesTo: 'all',
5909
+ test: (ast, componentName, componentSpec, options) => {
5910
+ const violations = [];
5911
+ // Common formatting methods that can fail on null/undefined
5912
+ const formattingMethods = new Set([
5913
+ // Number methods
5914
+ 'toFixed', 'toPrecision', 'toExponential',
5915
+ // Conversion methods
5916
+ 'toLocaleString', 'toString',
5917
+ // String methods
5918
+ 'toLowerCase', 'toUpperCase', 'trim',
5919
+ 'split', 'slice', 'substring', 'substr',
5920
+ 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf',
5921
+ 'padStart', 'padEnd', 'repeat', 'replace'
5922
+ ]);
5923
+ const checkFieldNullability = (propertyName) => {
5924
+ // Step 1: Check if componentSpec has data requirements and utilities are available
5925
+ if (!componentSpec?.dataRequirements?.entities || !options?.utilities?.md?.Entities) {
5926
+ return { found: false, nullable: false };
5927
+ }
5928
+ try {
5929
+ // Step 2: Iterate through only the entities defined in dataRequirements
5930
+ for (const dataReqEntity of componentSpec.dataRequirements.entities) {
5931
+ const entityName = dataReqEntity.name; // e.g., "AI Prompt Runs"
5932
+ // Step 3: Find this entity in the full metadata (case insensitive)
5933
+ // Use proper typing - we know Entities is an array of EntityInfo objects
5934
+ const fullEntity = options.utilities.md?.Entities.find((e) => e.Name && e.Name.toLowerCase() === entityName.toLowerCase());
5935
+ if (fullEntity && fullEntity.Fields && Array.isArray(fullEntity.Fields)) {
5936
+ // Step 4: Look for the field in this specific entity (case insensitive)
5937
+ const field = fullEntity.Fields.find((f) => f.Name && f.Name.trim().toLowerCase() === propertyName.trim().toLowerCase());
5938
+ if (field) {
5939
+ // Field found - check if it's nullable
5940
+ // In MJ, AllowsNull is a boolean property
5941
+ return {
5942
+ found: true,
5943
+ nullable: field.AllowsNull,
5944
+ entityName: fullEntity.Name,
5945
+ fieldName: field.Name
5946
+ };
5947
+ }
5948
+ }
5949
+ }
5950
+ }
5951
+ catch (error) {
5952
+ // If there's any error accessing metadata, fail gracefully
5953
+ console.warn('Error checking field nullability:', error);
5954
+ }
5955
+ return { found: false, nullable: false };
5956
+ };
5957
+ (0, traverse_1.default)(ast, {
5958
+ // Check JSX expressions
5959
+ JSXExpressionContainer(path) {
5960
+ const expr = path.node.expression;
5961
+ // Look for object.property.method() pattern
5962
+ if (t.isCallExpression(expr) &&
5963
+ t.isMemberExpression(expr.callee) &&
5964
+ t.isIdentifier(expr.callee.property)) {
5965
+ const methodName = expr.callee.property.name;
5966
+ // Check if it's a formatting method
5967
+ if (formattingMethods.has(methodName)) {
5968
+ const callee = expr.callee;
5969
+ // Check if the object being called on is also a member expression (x.y pattern)
5970
+ if (t.isMemberExpression(callee.object) &&
5971
+ t.isIdentifier(callee.object.property)) {
5972
+ const propertyName = callee.object.property.name;
5973
+ // Check if optional chaining is already used
5974
+ const hasOptionalChaining = callee.object.optional || callee.optional;
5975
+ // Check if there's a fallback (looking in parent for || or ??)
5976
+ let hasFallback = false;
5977
+ const parent = path.parent;
5978
+ const grandParent = path.parentPath?.parent;
5979
+ // Check if parent is a logical expression with fallback
5980
+ if (grandParent && t.isLogicalExpression(grandParent) &&
5981
+ (grandParent.operator === '||' || grandParent.operator === '??')) {
5982
+ hasFallback = true;
5983
+ }
5984
+ // Also check conditional expressions
5985
+ if (grandParent && t.isConditionalExpression(grandParent)) {
5986
+ hasFallback = true;
5987
+ }
5988
+ if (!hasOptionalChaining && !hasFallback) {
5989
+ // Check entity metadata for this field
5990
+ const fieldInfo = checkFieldNullability(propertyName);
5991
+ // Determine severity based on metadata
5992
+ let severity = 'medium';
5993
+ let message = `Unsafe formatting method '${methodName}()' called on '${propertyName}'. Consider using optional chaining.`;
5994
+ if (fieldInfo.found) {
5995
+ if (fieldInfo.nullable) {
5996
+ severity = 'high';
5997
+ message = `Field '${fieldInfo.fieldName}' from entity '${fieldInfo.entityName}' is nullable. Use optional chaining to prevent runtime errors when calling '${methodName}()'.`;
5998
+ }
5999
+ else {
6000
+ // Keep medium severity but note it's non-nullable
6001
+ message = `Field '${fieldInfo.fieldName}' from entity '${fieldInfo.entityName}' appears to be non-nullable, but consider using optional chaining for safety when calling '${methodName}()'.`;
6002
+ }
6003
+ }
6004
+ // Get the object name for better error message
6005
+ let objectName = '';
6006
+ if (t.isIdentifier(callee.object.object)) {
6007
+ objectName = callee.object.object.name;
6008
+ }
6009
+ violations.push({
6010
+ rule: 'unsafe-formatting-methods',
6011
+ severity: severity,
6012
+ line: expr.loc?.start.line || 0,
6013
+ column: expr.loc?.start.column || 0,
6014
+ message: message,
6015
+ code: `${objectName}.${propertyName}.${methodName}() → ${objectName}.${propertyName}?.${methodName}() ?? defaultValue`
6016
+ });
6017
+ }
6018
+ }
6019
+ }
6020
+ }
6021
+ },
6022
+ // Also check template literals
6023
+ TemplateLiteral(path) {
6024
+ for (const expr of path.node.expressions) {
6025
+ // Look for object.property.method() pattern in template expressions
6026
+ if (t.isCallExpression(expr) &&
6027
+ t.isMemberExpression(expr.callee) &&
6028
+ t.isIdentifier(expr.callee.property)) {
6029
+ const methodName = expr.callee.property.name;
6030
+ // Check if it's a formatting method
6031
+ if (formattingMethods.has(methodName)) {
6032
+ const callee = expr.callee;
6033
+ // Check if the object being called on is also a member expression (x.y pattern)
6034
+ if (t.isMemberExpression(callee.object) &&
6035
+ t.isIdentifier(callee.object.property)) {
6036
+ const propertyName = callee.object.property.name;
6037
+ // Check if optional chaining is already used
6038
+ const hasOptionalChaining = callee.object.optional || callee.optional;
6039
+ if (!hasOptionalChaining) {
6040
+ // Check entity metadata for this field
6041
+ const fieldInfo = checkFieldNullability(propertyName);
6042
+ // Determine severity based on metadata
6043
+ let severity = 'medium';
6044
+ let message = `Unsafe formatting method '${methodName}()' called on '${propertyName}' in template literal. Consider using optional chaining.`;
6045
+ if (fieldInfo.found) {
6046
+ if (fieldInfo.nullable) {
6047
+ severity = 'high';
6048
+ message = `Field '${propertyName}' is nullable in entity metadata. Use optional chaining to prevent runtime errors when calling '${methodName}()' in template literal.`;
6049
+ }
6050
+ else {
6051
+ // Keep medium severity but note it's non-nullable
6052
+ message = `Field '${propertyName}' appears to be non-nullable, but consider using optional chaining for safety when calling '${methodName}()' in template literal.`;
6053
+ }
6054
+ }
6055
+ // Get the object name for better error message
6056
+ let objectName = '';
6057
+ if (t.isIdentifier(callee.object.object)) {
6058
+ objectName = callee.object.object.name;
6059
+ }
6060
+ violations.push({
6061
+ rule: 'unsafe-formatting-methods',
6062
+ severity: severity,
6063
+ line: expr.loc?.start.line || 0,
6064
+ column: expr.loc?.start.column || 0,
6065
+ message: message,
6066
+ code: `\${${objectName}.${propertyName}.${methodName}()} → \${${objectName}.${propertyName}?.${methodName}() ?? defaultValue}`
6067
+ });
6068
+ }
6069
+ }
6070
+ }
6071
+ }
6072
+ }
6073
+ }
6074
+ });
6075
+ return violations;
6076
+ }
5638
6077
  }
5639
6078
  ];
5640
6079
  //# sourceMappingURL=component-linter.js.map