@memberjunction/react-test-harness 2.92.0 → 2.93.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,CAi7IpC;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
@@ -5635,6 +5635,443 @@ ComponentLinter.universalComponentRules = [
5635
5635
  });
5636
5636
  return violations;
5637
5637
  }
5638
+ },
5639
+ {
5640
+ name: 'utilities-valid-properties',
5641
+ appliesTo: 'all',
5642
+ test: (ast, componentName, componentSpec) => {
5643
+ const violations = [];
5644
+ const validProperties = new Set(['rv', 'rq', 'md', 'ai']);
5645
+ (0, traverse_1.default)(ast, {
5646
+ MemberExpression(path) {
5647
+ // Check for utilities.* access
5648
+ if (t.isIdentifier(path.node.object) && path.node.object.name === 'utilities') {
5649
+ if (t.isIdentifier(path.node.property)) {
5650
+ const propName = path.node.property.name;
5651
+ // Check if it's a valid property
5652
+ if (!validProperties.has(propName)) {
5653
+ violations.push({
5654
+ rule: 'utilities-valid-properties',
5655
+ severity: 'critical',
5656
+ line: path.node.loc?.start.line || 0,
5657
+ column: path.node.loc?.start.column || 0,
5658
+ message: `Invalid utilities property '${propName}'. Valid properties are: rv (RunView), rq (RunQuery), md (Metadata), ai (AI Tools)`,
5659
+ code: `utilities.${propName}`
5660
+ });
5661
+ }
5662
+ }
5663
+ }
5664
+ }
5665
+ });
5666
+ return violations;
5667
+ }
5668
+ },
5669
+ {
5670
+ name: 'utilities-runview-methods',
5671
+ appliesTo: 'all',
5672
+ test: (ast, componentName, componentSpec) => {
5673
+ const violations = [];
5674
+ const validMethods = new Set(['RunView', 'RunViews']);
5675
+ (0, traverse_1.default)(ast, {
5676
+ CallExpression(path) {
5677
+ // Check for utilities.rv.* method calls
5678
+ if (t.isMemberExpression(path.node.callee)) {
5679
+ const callee = path.node.callee;
5680
+ // Check if it's utilities.rv.methodName()
5681
+ if (t.isMemberExpression(callee.object) &&
5682
+ t.isIdentifier(callee.object.object) &&
5683
+ callee.object.object.name === 'utilities' &&
5684
+ t.isIdentifier(callee.object.property) &&
5685
+ callee.object.property.name === 'rv' &&
5686
+ t.isIdentifier(callee.property)) {
5687
+ const methodName = callee.property.name;
5688
+ if (!validMethods.has(methodName)) {
5689
+ violations.push({
5690
+ rule: 'utilities-runview-methods',
5691
+ severity: 'critical',
5692
+ line: path.node.loc?.start.line || 0,
5693
+ column: path.node.loc?.start.column || 0,
5694
+ message: `Invalid method '${methodName}' on utilities.rv. Valid methods are: RunView, RunViews`,
5695
+ code: `utilities.rv.${methodName}()`
5696
+ });
5697
+ }
5698
+ }
5699
+ }
5700
+ }
5701
+ });
5702
+ return violations;
5703
+ }
5704
+ },
5705
+ {
5706
+ name: 'utilities-runquery-methods',
5707
+ appliesTo: 'all',
5708
+ test: (ast, componentName, componentSpec) => {
5709
+ const violations = [];
5710
+ const validMethods = new Set(['RunQuery']);
5711
+ (0, traverse_1.default)(ast, {
5712
+ CallExpression(path) {
5713
+ // Check for utilities.rq.* method calls
5714
+ if (t.isMemberExpression(path.node.callee)) {
5715
+ const callee = path.node.callee;
5716
+ // Check if it's utilities.rq.methodName()
5717
+ if (t.isMemberExpression(callee.object) &&
5718
+ t.isIdentifier(callee.object.object) &&
5719
+ callee.object.object.name === 'utilities' &&
5720
+ t.isIdentifier(callee.object.property) &&
5721
+ callee.object.property.name === 'rq' &&
5722
+ t.isIdentifier(callee.property)) {
5723
+ const methodName = callee.property.name;
5724
+ if (!validMethods.has(methodName)) {
5725
+ violations.push({
5726
+ rule: 'utilities-runquery-methods',
5727
+ severity: 'critical',
5728
+ line: path.node.loc?.start.line || 0,
5729
+ column: path.node.loc?.start.column || 0,
5730
+ message: `Invalid method '${methodName}' on utilities.rq. Valid method is: RunQuery`,
5731
+ code: `utilities.rq.${methodName}()`
5732
+ });
5733
+ }
5734
+ }
5735
+ }
5736
+ }
5737
+ });
5738
+ return violations;
5739
+ }
5740
+ },
5741
+ {
5742
+ name: 'utilities-metadata-methods',
5743
+ appliesTo: 'all',
5744
+ test: (ast, componentName, componentSpec) => {
5745
+ const violations = [];
5746
+ const validMethods = new Set(['GetEntityObject']);
5747
+ const validProperties = new Set(['Entities']);
5748
+ (0, traverse_1.default)(ast, {
5749
+ // Check for method calls
5750
+ CallExpression(path) {
5751
+ // Check for utilities.md.* method calls
5752
+ if (t.isMemberExpression(path.node.callee)) {
5753
+ const callee = path.node.callee;
5754
+ // Check if it's utilities.md.methodName()
5755
+ if (t.isMemberExpression(callee.object) &&
5756
+ t.isIdentifier(callee.object.object) &&
5757
+ callee.object.object.name === 'utilities' &&
5758
+ t.isIdentifier(callee.object.property) &&
5759
+ callee.object.property.name === 'md' &&
5760
+ t.isIdentifier(callee.property)) {
5761
+ const methodName = callee.property.name;
5762
+ if (!validMethods.has(methodName)) {
5763
+ violations.push({
5764
+ rule: 'utilities-metadata-methods',
5765
+ severity: 'critical',
5766
+ line: path.node.loc?.start.line || 0,
5767
+ column: path.node.loc?.start.column || 0,
5768
+ message: `Invalid method '${methodName}' on utilities.md. Valid methods are: GetEntityObject. Valid properties are: Entities`,
5769
+ code: `utilities.md.${methodName}()`
5770
+ });
5771
+ }
5772
+ }
5773
+ }
5774
+ },
5775
+ // Check for property access (non-call expressions)
5776
+ MemberExpression(path) {
5777
+ // Skip if this is part of a call expression (handled above)
5778
+ if (t.isCallExpression(path.parent) && path.parent.callee === path.node) {
5779
+ return;
5780
+ }
5781
+ // Check if it's utilities.md.propertyName
5782
+ if (t.isMemberExpression(path.node.object) &&
5783
+ t.isIdentifier(path.node.object.object) &&
5784
+ path.node.object.object.name === 'utilities' &&
5785
+ t.isIdentifier(path.node.object.property) &&
5786
+ path.node.object.property.name === 'md' &&
5787
+ t.isIdentifier(path.node.property)) {
5788
+ const propName = path.node.property.name;
5789
+ // Check if it's accessing a valid property or trying to access an invalid one
5790
+ if (!validProperties.has(propName) && !validMethods.has(propName)) {
5791
+ violations.push({
5792
+ rule: 'utilities-metadata-methods',
5793
+ severity: 'critical',
5794
+ line: path.node.loc?.start.line || 0,
5795
+ column: path.node.loc?.start.column || 0,
5796
+ message: `Invalid property '${propName}' on utilities.md. Valid methods are: GetEntityObject. Valid properties are: Entities`,
5797
+ code: `utilities.md.${propName}`
5798
+ });
5799
+ }
5800
+ }
5801
+ }
5802
+ });
5803
+ return violations;
5804
+ }
5805
+ },
5806
+ {
5807
+ name: 'utilities-ai-methods',
5808
+ appliesTo: 'all',
5809
+ test: (ast, componentName, componentSpec) => {
5810
+ const violations = [];
5811
+ const validMethods = new Set(['ExecutePrompt', 'EmbedText']);
5812
+ const validProperties = new Set(['VectorService']);
5813
+ (0, traverse_1.default)(ast, {
5814
+ // Check for method calls
5815
+ CallExpression(path) {
5816
+ // Check for utilities.ai.* method calls
5817
+ if (t.isMemberExpression(path.node.callee)) {
5818
+ const callee = path.node.callee;
5819
+ // Check if it's utilities.ai.methodName()
5820
+ if (t.isMemberExpression(callee.object) &&
5821
+ t.isIdentifier(callee.object.object) &&
5822
+ callee.object.object.name === 'utilities' &&
5823
+ t.isIdentifier(callee.object.property) &&
5824
+ callee.object.property.name === 'ai' &&
5825
+ t.isIdentifier(callee.property)) {
5826
+ const methodName = callee.property.name;
5827
+ if (!validMethods.has(methodName)) {
5828
+ violations.push({
5829
+ rule: 'utilities-ai-methods',
5830
+ severity: 'critical',
5831
+ line: path.node.loc?.start.line || 0,
5832
+ column: path.node.loc?.start.column || 0,
5833
+ message: `Invalid method '${methodName}' on utilities.ai. Valid methods are: ExecutePrompt, EmbedText. Valid property: VectorService`,
5834
+ code: `utilities.ai.${methodName}()`
5835
+ });
5836
+ }
5837
+ }
5838
+ }
5839
+ },
5840
+ // Check for property access (VectorService)
5841
+ MemberExpression(path) {
5842
+ // Skip if this is part of a call expression (handled above)
5843
+ if (t.isCallExpression(path.parent)) {
5844
+ return;
5845
+ }
5846
+ // Check if it's utilities.ai.propertyName
5847
+ if (t.isMemberExpression(path.node.object) &&
5848
+ t.isIdentifier(path.node.object.object) &&
5849
+ path.node.object.object.name === 'utilities' &&
5850
+ t.isIdentifier(path.node.object.property) &&
5851
+ path.node.object.property.name === 'ai' &&
5852
+ t.isIdentifier(path.node.property)) {
5853
+ const propName = path.node.property.name;
5854
+ // Check if it's a valid property or method (methods might be referenced without calling)
5855
+ if (!validProperties.has(propName) && !validMethods.has(propName)) {
5856
+ violations.push({
5857
+ rule: 'utilities-ai-properties',
5858
+ severity: 'critical',
5859
+ line: path.node.loc?.start.line || 0,
5860
+ column: path.node.loc?.start.column || 0,
5861
+ message: `Invalid property '${propName}' on utilities.ai. Valid methods are: ExecutePrompt, EmbedText. Valid property: VectorService`,
5862
+ code: `utilities.ai.${propName}`
5863
+ });
5864
+ }
5865
+ }
5866
+ }
5867
+ });
5868
+ return violations;
5869
+ }
5870
+ },
5871
+ {
5872
+ name: 'utilities-no-direct-instantiation',
5873
+ appliesTo: 'all',
5874
+ test: (ast, componentName, componentSpec) => {
5875
+ const violations = [];
5876
+ const restrictedClasses = new Map([
5877
+ ['RunView', 'utilities.rv'],
5878
+ ['RunQuery', 'utilities.rq'],
5879
+ ['Metadata', 'utilities.md'],
5880
+ ['SimpleVectorService', 'utilities.ai.VectorService']
5881
+ ]);
5882
+ (0, traverse_1.default)(ast, {
5883
+ NewExpression(path) {
5884
+ // Check if instantiating a restricted class
5885
+ if (t.isIdentifier(path.node.callee)) {
5886
+ const className = path.node.callee.name;
5887
+ if (restrictedClasses.has(className)) {
5888
+ const utilityPath = restrictedClasses.get(className);
5889
+ violations.push({
5890
+ rule: 'utilities-no-direct-instantiation',
5891
+ severity: 'high',
5892
+ line: path.node.loc?.start.line || 0,
5893
+ column: path.node.loc?.start.column || 0,
5894
+ message: `Don't instantiate ${className} directly. Use ${utilityPath} instead which is provided in the component's utilities parameter.`,
5895
+ code: `new ${className}()`
5896
+ });
5897
+ }
5898
+ }
5899
+ }
5900
+ });
5901
+ return violations;
5902
+ }
5903
+ },
5904
+ {
5905
+ name: 'unsafe-formatting-methods',
5906
+ appliesTo: 'all',
5907
+ test: (ast, componentName, componentSpec, options) => {
5908
+ const violations = [];
5909
+ // Common formatting methods that can fail on null/undefined
5910
+ const formattingMethods = new Set([
5911
+ // Number methods
5912
+ 'toFixed', 'toPrecision', 'toExponential',
5913
+ // Conversion methods
5914
+ 'toLocaleString', 'toString',
5915
+ // String methods
5916
+ 'toLowerCase', 'toUpperCase', 'trim',
5917
+ 'split', 'slice', 'substring', 'substr',
5918
+ 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf',
5919
+ 'padStart', 'padEnd', 'repeat', 'replace'
5920
+ ]);
5921
+ const checkFieldNullability = (propertyName) => {
5922
+ // Step 1: Check if componentSpec has data requirements and utilities are available
5923
+ if (!componentSpec?.dataRequirements?.entities || !options?.utilities?.md?.Entities) {
5924
+ return { found: false, nullable: false };
5925
+ }
5926
+ try {
5927
+ // Step 2: Iterate through only the entities defined in dataRequirements
5928
+ for (const dataReqEntity of componentSpec.dataRequirements.entities) {
5929
+ const entityName = dataReqEntity.name; // e.g., "AI Prompt Runs"
5930
+ // Step 3: Find this entity in the full metadata (case insensitive)
5931
+ // Use proper typing - we know Entities is an array of EntityInfo objects
5932
+ const fullEntity = options.utilities.md?.Entities.find((e) => e.Name && e.Name.toLowerCase() === entityName.toLowerCase());
5933
+ if (fullEntity && fullEntity.Fields && Array.isArray(fullEntity.Fields)) {
5934
+ // Step 4: Look for the field in this specific entity (case insensitive)
5935
+ const field = fullEntity.Fields.find((f) => f.Name && f.Name.trim().toLowerCase() === propertyName.trim().toLowerCase());
5936
+ if (field) {
5937
+ // Field found - check if it's nullable
5938
+ // In MJ, AllowsNull is a boolean property
5939
+ return {
5940
+ found: true,
5941
+ nullable: field.AllowsNull,
5942
+ entityName: fullEntity.Name,
5943
+ fieldName: field.Name
5944
+ };
5945
+ }
5946
+ }
5947
+ }
5948
+ }
5949
+ catch (error) {
5950
+ // If there's any error accessing metadata, fail gracefully
5951
+ console.warn('Error checking field nullability:', error);
5952
+ }
5953
+ return { found: false, nullable: false };
5954
+ };
5955
+ (0, traverse_1.default)(ast, {
5956
+ // Check JSX expressions
5957
+ JSXExpressionContainer(path) {
5958
+ const expr = path.node.expression;
5959
+ // Look for object.property.method() pattern
5960
+ if (t.isCallExpression(expr) &&
5961
+ t.isMemberExpression(expr.callee) &&
5962
+ t.isIdentifier(expr.callee.property)) {
5963
+ const methodName = expr.callee.property.name;
5964
+ // Check if it's a formatting method
5965
+ if (formattingMethods.has(methodName)) {
5966
+ const callee = expr.callee;
5967
+ // Check if the object being called on is also a member expression (x.y pattern)
5968
+ if (t.isMemberExpression(callee.object) &&
5969
+ t.isIdentifier(callee.object.property)) {
5970
+ const propertyName = callee.object.property.name;
5971
+ // Check if optional chaining is already used
5972
+ const hasOptionalChaining = callee.object.optional || callee.optional;
5973
+ // Check if there's a fallback (looking in parent for || or ??)
5974
+ let hasFallback = false;
5975
+ const parent = path.parent;
5976
+ const grandParent = path.parentPath?.parent;
5977
+ // Check if parent is a logical expression with fallback
5978
+ if (grandParent && t.isLogicalExpression(grandParent) &&
5979
+ (grandParent.operator === '||' || grandParent.operator === '??')) {
5980
+ hasFallback = true;
5981
+ }
5982
+ // Also check conditional expressions
5983
+ if (grandParent && t.isConditionalExpression(grandParent)) {
5984
+ hasFallback = true;
5985
+ }
5986
+ if (!hasOptionalChaining && !hasFallback) {
5987
+ // Check entity metadata for this field
5988
+ const fieldInfo = checkFieldNullability(propertyName);
5989
+ // Determine severity based on metadata
5990
+ let severity = 'medium';
5991
+ let message = `Unsafe formatting method '${methodName}()' called on '${propertyName}'. Consider using optional chaining.`;
5992
+ if (fieldInfo.found) {
5993
+ if (fieldInfo.nullable) {
5994
+ severity = 'high';
5995
+ message = `Field '${fieldInfo.fieldName}' from entity '${fieldInfo.entityName}' is nullable. Use optional chaining to prevent runtime errors when calling '${methodName}()'.`;
5996
+ }
5997
+ else {
5998
+ // Keep medium severity but note it's non-nullable
5999
+ message = `Field '${fieldInfo.fieldName}' from entity '${fieldInfo.entityName}' appears to be non-nullable, but consider using optional chaining for safety when calling '${methodName}()'.`;
6000
+ }
6001
+ }
6002
+ // Get the object name for better error message
6003
+ let objectName = '';
6004
+ if (t.isIdentifier(callee.object.object)) {
6005
+ objectName = callee.object.object.name;
6006
+ }
6007
+ violations.push({
6008
+ rule: 'unsafe-formatting-methods',
6009
+ severity: severity,
6010
+ line: expr.loc?.start.line || 0,
6011
+ column: expr.loc?.start.column || 0,
6012
+ message: message,
6013
+ code: `${objectName}.${propertyName}.${methodName}() → ${objectName}.${propertyName}?.${methodName}() ?? defaultValue`
6014
+ });
6015
+ }
6016
+ }
6017
+ }
6018
+ }
6019
+ },
6020
+ // Also check template literals
6021
+ TemplateLiteral(path) {
6022
+ for (const expr of path.node.expressions) {
6023
+ // Look for object.property.method() pattern in template expressions
6024
+ if (t.isCallExpression(expr) &&
6025
+ t.isMemberExpression(expr.callee) &&
6026
+ t.isIdentifier(expr.callee.property)) {
6027
+ const methodName = expr.callee.property.name;
6028
+ // Check if it's a formatting method
6029
+ if (formattingMethods.has(methodName)) {
6030
+ const callee = expr.callee;
6031
+ // Check if the object being called on is also a member expression (x.y pattern)
6032
+ if (t.isMemberExpression(callee.object) &&
6033
+ t.isIdentifier(callee.object.property)) {
6034
+ const propertyName = callee.object.property.name;
6035
+ // Check if optional chaining is already used
6036
+ const hasOptionalChaining = callee.object.optional || callee.optional;
6037
+ if (!hasOptionalChaining) {
6038
+ // Check entity metadata for this field
6039
+ const fieldInfo = checkFieldNullability(propertyName);
6040
+ // Determine severity based on metadata
6041
+ let severity = 'medium';
6042
+ let message = `Unsafe formatting method '${methodName}()' called on '${propertyName}' in template literal. Consider using optional chaining.`;
6043
+ if (fieldInfo.found) {
6044
+ if (fieldInfo.nullable) {
6045
+ severity = 'high';
6046
+ message = `Field '${propertyName}' is nullable in entity metadata. Use optional chaining to prevent runtime errors when calling '${methodName}()' in template literal.`;
6047
+ }
6048
+ else {
6049
+ // Keep medium severity but note it's non-nullable
6050
+ message = `Field '${propertyName}' appears to be non-nullable, but consider using optional chaining for safety when calling '${methodName}()' in template literal.`;
6051
+ }
6052
+ }
6053
+ // Get the object name for better error message
6054
+ let objectName = '';
6055
+ if (t.isIdentifier(callee.object.object)) {
6056
+ objectName = callee.object.object.name;
6057
+ }
6058
+ violations.push({
6059
+ rule: 'unsafe-formatting-methods',
6060
+ severity: severity,
6061
+ line: expr.loc?.start.line || 0,
6062
+ column: expr.loc?.start.column || 0,
6063
+ message: message,
6064
+ code: `\${${objectName}.${propertyName}.${methodName}()} → \${${objectName}.${propertyName}?.${methodName}() ?? defaultValue}`
6065
+ });
6066
+ }
6067
+ }
6068
+ }
6069
+ }
6070
+ }
6071
+ }
6072
+ });
6073
+ return violations;
6074
+ }
5638
6075
  }
5639
6076
  ];
5640
6077
  //# sourceMappingURL=component-linter.js.map