@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.
- 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 +457 -18
- 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 +238 -20
- 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,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
|
-
'
|
|
3653
|
-
'MaxRows', 'StartRow', 'ResultType'
|
|
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
|
|
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
|
|
3776
|
-
if (!
|
|
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
|
|
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
|