@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.
- package/dist/lib/component-linter.d.ts +2 -1
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +439 -2
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +5 -11
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +223 -15
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/linter-test-tool.d.ts +42 -0
- package/dist/lib/linter-test-tool.d.ts.map +1 -0
- package/dist/lib/linter-test-tool.js +272 -0
- package/dist/lib/linter-test-tool.js.map +1 -0
- package/dist/lib/test-harness.d.ts.map +1 -1
- package/dist/lib/test-harness.js +1 -1
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -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;
|
|
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
|