@memberjunction/react-test-harness 2.123.1 → 2.125.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.map +1 -1
- package/dist/lib/component-linter.js +433 -2
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/constraint-validators/base-constraint-validator.d.ts +259 -0
- package/dist/lib/constraint-validators/base-constraint-validator.d.ts.map +1 -0
- package/dist/lib/constraint-validators/base-constraint-validator.js +304 -0
- package/dist/lib/constraint-validators/base-constraint-validator.js.map +1 -0
- package/dist/lib/constraint-validators/index.d.ts +21 -0
- package/dist/lib/constraint-validators/index.d.ts.map +1 -0
- package/dist/lib/constraint-validators/index.js +37 -0
- package/dist/lib/constraint-validators/index.js.map +1 -0
- package/dist/lib/constraint-validators/required-when-validator.d.ts +43 -0
- package/dist/lib/constraint-validators/required-when-validator.d.ts.map +1 -0
- package/dist/lib/constraint-validators/required-when-validator.js +97 -0
- package/dist/lib/constraint-validators/required-when-validator.js.map +1 -0
- package/dist/lib/constraint-validators/sql-where-clause-validator.d.ts +116 -0
- package/dist/lib/constraint-validators/sql-where-clause-validator.d.ts.map +1 -0
- package/dist/lib/constraint-validators/sql-where-clause-validator.js +381 -0
- package/dist/lib/constraint-validators/sql-where-clause-validator.js.map +1 -0
- package/dist/lib/constraint-validators/subset-of-entity-fields-validator.d.ts +60 -0
- package/dist/lib/constraint-validators/subset-of-entity-fields-validator.d.ts.map +1 -0
- package/dist/lib/constraint-validators/subset-of-entity-fields-validator.js +198 -0
- package/dist/lib/constraint-validators/subset-of-entity-fields-validator.js.map +1 -0
- package/dist/lib/constraint-validators/validation-context.d.ts +326 -0
- package/dist/lib/constraint-validators/validation-context.d.ts.map +1 -0
- package/dist/lib/constraint-validators/validation-context.js +14 -0
- package/dist/lib/constraint-validators/validation-context.js.map +1 -0
- package/dist/lib/prop-value-extractor.d.ts +147 -0
- package/dist/lib/prop-value-extractor.d.ts.map +1 -0
- package/dist/lib/prop-value-extractor.js +499 -0
- package/dist/lib/prop-value-extractor.js.map +1 -0
- package/dist/lib/type-context.d.ts +2 -0
- package/dist/lib/type-context.d.ts.map +1 -1
- package/dist/lib/type-context.js +22 -9
- package/dist/lib/type-context.js.map +1 -1
- package/dist/lib/type-inference-engine.d.ts +20 -0
- package/dist/lib/type-inference-engine.d.ts.map +1 -1
- package/dist/lib/type-inference-engine.js +253 -20
- package/dist/lib/type-inference-engine.js.map +1 -1
- package/package.json +13 -9
|
@@ -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;AAI3G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,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;AAI3G,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAU/D,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;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;IACd,MAAM,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,cAAc,CAAC;IACnF,UAAU,CAAC,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AA0OD,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,cAAc,CAAqB;IAGlD,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAQhC,OAAO,CAAC,MAAM,CAAC,cAAc;IA8B7B,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAyC3C,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAg2PpC;WAEkB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;WAmC3G,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;IA4KtB,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAsWvC,OAAO,CAAC,MAAM,CAAC,eAAe;IA2B9B,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAyBpC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,0BAA0B;IA4hCzC,OAAO,CAAC,MAAM,CAAC,8BAA8B;IA0B7C;;OAEG;mBACkB,qBAAqB;IA0H1C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAqDzC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,6BAA6B;IAwB5C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA+BlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA+KpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA2DlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;CAqGzC"}
|
|
@@ -36,6 +36,9 @@ const styles_type_analyzer_1 = require("./styles-type-analyzer");
|
|
|
36
36
|
const type_context_1 = require("./type-context");
|
|
37
37
|
const type_inference_engine_1 = require("./type-inference-engine");
|
|
38
38
|
const control_flow_analyzer_1 = require("./control-flow-analyzer");
|
|
39
|
+
const constraint_validators_1 = require("./constraint-validators");
|
|
40
|
+
const prop_value_extractor_1 = require("./prop-value-extractor");
|
|
41
|
+
const global_1 = require("@memberjunction/global");
|
|
39
42
|
// Standard HTML elements (lowercase)
|
|
40
43
|
const HTML_ELEMENTS = new Set([
|
|
41
44
|
// Main root
|
|
@@ -3860,6 +3863,192 @@ ComponentLinter.universalComponentRules = [
|
|
|
3860
3863
|
return violations;
|
|
3861
3864
|
},
|
|
3862
3865
|
},
|
|
3866
|
+
{
|
|
3867
|
+
name: 'useeffect-unstable-dependencies',
|
|
3868
|
+
appliesTo: 'all',
|
|
3869
|
+
test: (ast, componentName, componentSpec) => {
|
|
3870
|
+
const violations = [];
|
|
3871
|
+
// Known prop names that are always objects/functions and unstable
|
|
3872
|
+
const unstablePropNames = new Set([
|
|
3873
|
+
'utilities',
|
|
3874
|
+
'components',
|
|
3875
|
+
'callbacks',
|
|
3876
|
+
'styles',
|
|
3877
|
+
'savedUserSettings', // Can be unstable if not memoized by parent
|
|
3878
|
+
]);
|
|
3879
|
+
// Helper to find the component function and extract parameters with object defaults
|
|
3880
|
+
const findComponentParams = (useEffectPath) => {
|
|
3881
|
+
const paramsWithObjectDefaults = new Map();
|
|
3882
|
+
let current = useEffectPath.parentPath;
|
|
3883
|
+
while (current) {
|
|
3884
|
+
// Look for FunctionDeclaration or ArrowFunctionExpression/FunctionExpression
|
|
3885
|
+
if (t.isFunctionDeclaration(current.node) ||
|
|
3886
|
+
t.isArrowFunctionExpression(current.node) ||
|
|
3887
|
+
t.isFunctionExpression(current.node)) {
|
|
3888
|
+
const func = current.node;
|
|
3889
|
+
// Check if this looks like a component (starts with uppercase)
|
|
3890
|
+
let isComponent = false;
|
|
3891
|
+
if (t.isFunctionDeclaration(func) && func.id && /^[A-Z]/.test(func.id.name)) {
|
|
3892
|
+
isComponent = true;
|
|
3893
|
+
}
|
|
3894
|
+
// For arrow functions, check the variable declarator name
|
|
3895
|
+
if ((t.isArrowFunctionExpression(func) || t.isFunctionExpression(func)) && current.parentPath) {
|
|
3896
|
+
const parent = current.parentPath.node;
|
|
3897
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id) && /^[A-Z]/.test(parent.id.name)) {
|
|
3898
|
+
isComponent = true;
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
if (isComponent) {
|
|
3902
|
+
// Extract parameters with object literal defaults
|
|
3903
|
+
for (const param of func.params) {
|
|
3904
|
+
// Case 1: ObjectPattern (destructured props): { foo = {}, bar = [] }
|
|
3905
|
+
if (t.isObjectPattern(param)) {
|
|
3906
|
+
for (const prop of param.properties) {
|
|
3907
|
+
if (t.isObjectProperty(prop)) {
|
|
3908
|
+
const value = prop.value;
|
|
3909
|
+
// Check if this destructured property has a default: queryParameters = {}
|
|
3910
|
+
if (t.isAssignmentPattern(value) && t.isIdentifier(value.left)) {
|
|
3911
|
+
const defaultVal = value.right;
|
|
3912
|
+
if (t.isObjectExpression(defaultVal) || t.isArrayExpression(defaultVal)) {
|
|
3913
|
+
paramsWithObjectDefaults.set(value.left.name, defaultVal);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
// Case 2: AssignmentPattern (param with default): queryParameters = {}
|
|
3920
|
+
else if (t.isAssignmentPattern(param)) {
|
|
3921
|
+
const left = param.left;
|
|
3922
|
+
const right = param.right;
|
|
3923
|
+
// Simple param with object default: queryParameters = {}
|
|
3924
|
+
if (t.isIdentifier(left) && (t.isObjectExpression(right) || t.isArrayExpression(right))) {
|
|
3925
|
+
paramsWithObjectDefaults.set(left.name, right);
|
|
3926
|
+
}
|
|
3927
|
+
// ObjectPattern with object default: { foo, bar } = {}
|
|
3928
|
+
else if (t.isObjectPattern(left) && (t.isObjectExpression(right) || t.isArrayExpression(right))) {
|
|
3929
|
+
// The whole destructured object gets a default - mark all properties
|
|
3930
|
+
for (const prop of left.properties) {
|
|
3931
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
3932
|
+
paramsWithObjectDefaults.set(prop.key.name, right);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
break; // Found the component, stop traversing up
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
current = current.parentPath;
|
|
3942
|
+
}
|
|
3943
|
+
return paramsWithObjectDefaults;
|
|
3944
|
+
};
|
|
3945
|
+
(0, traverse_1.default)(ast, {
|
|
3946
|
+
CallExpression(path) {
|
|
3947
|
+
// Check for useEffect calls
|
|
3948
|
+
if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useEffect') {
|
|
3949
|
+
// Get the dependency array (second argument)
|
|
3950
|
+
const depsArg = path.node.arguments[1];
|
|
3951
|
+
if (!depsArg || !t.isArrayExpression(depsArg)) {
|
|
3952
|
+
return; // No deps array or empty deps []
|
|
3953
|
+
}
|
|
3954
|
+
// Find component parameters with object defaults
|
|
3955
|
+
const paramsWithObjectDefaults = findComponentParams(path);
|
|
3956
|
+
// Check each dependency
|
|
3957
|
+
for (const dep of depsArg.elements) {
|
|
3958
|
+
if (!dep)
|
|
3959
|
+
continue;
|
|
3960
|
+
let unstableDep = null;
|
|
3961
|
+
let severity = 'high';
|
|
3962
|
+
let message = '';
|
|
3963
|
+
let suggestionText = '';
|
|
3964
|
+
// Case 1: Member expression (utilities.rq.RunQuery, callbacks?.onSelect)
|
|
3965
|
+
if (t.isMemberExpression(dep) || t.isOptionalMemberExpression(dep)) {
|
|
3966
|
+
const memberExpr = dep;
|
|
3967
|
+
// Get the root object (e.g., 'utilities' from 'utilities.rq.RunQuery')
|
|
3968
|
+
let rootObj = memberExpr.object;
|
|
3969
|
+
while ((t.isMemberExpression(rootObj) || t.isOptionalMemberExpression(rootObj)) && 'object' in rootObj) {
|
|
3970
|
+
rootObj = rootObj.object;
|
|
3971
|
+
}
|
|
3972
|
+
if (t.isIdentifier(rootObj) && unstablePropNames.has(rootObj.name)) {
|
|
3973
|
+
unstableDep = `${rootObj.name}.${t.isIdentifier(memberExpr.property) ? memberExpr.property.name : '...'}`;
|
|
3974
|
+
severity = 'high';
|
|
3975
|
+
message = `useEffect has unstable dependency '${unstableDep}' that may cause infinite render loops. Object/function references from props typically change on every render. This works if the parent provides stable references (via useMemo), but is fragile and should be avoided.`;
|
|
3976
|
+
suggestionText = `Remove '${unstableDep}' from dependency array. These utilities/services are typically stable and don't need to be tracked.`;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
// Case 2: Direct identifier (utilities, components, etc.)
|
|
3980
|
+
else if (t.isIdentifier(dep)) {
|
|
3981
|
+
// Check if it's a known unstable prop name
|
|
3982
|
+
if (unstablePropNames.has(dep.name)) {
|
|
3983
|
+
unstableDep = dep.name;
|
|
3984
|
+
severity = 'high';
|
|
3985
|
+
message = `useEffect has unstable dependency '${unstableDep}' that may cause infinite render loops. Object/function references from props typically change on every render. This works if the parent provides stable references (via useMemo), but is fragile and should be avoided.`;
|
|
3986
|
+
suggestionText = `Remove '${unstableDep}' from dependency array. These utilities/services are typically stable and don't need to be tracked.`;
|
|
3987
|
+
}
|
|
3988
|
+
// Check if it's a param with object literal default
|
|
3989
|
+
else if (paramsWithObjectDefaults.has(dep.name)) {
|
|
3990
|
+
unstableDep = dep.name;
|
|
3991
|
+
severity = 'critical';
|
|
3992
|
+
const defaultValue = paramsWithObjectDefaults.get(dep.name);
|
|
3993
|
+
const defaultStr = t.isObjectExpression(defaultValue) ? '{}' : '[]';
|
|
3994
|
+
message = `useEffect has CRITICAL unstable dependency '${unstableDep}' with object literal default (${dep.name} = ${defaultStr}). This creates a NEW object on EVERY render, causing infinite loops. This is ALWAYS broken.`;
|
|
3995
|
+
suggestionText = `Remove '${unstableDep}' from dependency array. Props with object literal defaults (${dep.name} = ${defaultStr}) create new references every render.`;
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
// Report violation if we found an unstable dependency
|
|
3999
|
+
if (unstableDep) {
|
|
4000
|
+
let fixedDeps = depsArg.elements
|
|
4001
|
+
.filter((e) => e !== dep)
|
|
4002
|
+
.map((e) => {
|
|
4003
|
+
if (!e)
|
|
4004
|
+
return '';
|
|
4005
|
+
if (t.isIdentifier(e))
|
|
4006
|
+
return e.name;
|
|
4007
|
+
if (t.isMemberExpression(e) || t.isOptionalMemberExpression(e)) {
|
|
4008
|
+
// Try to extract the full path
|
|
4009
|
+
const parts = [];
|
|
4010
|
+
let current = e;
|
|
4011
|
+
while (t.isMemberExpression(current) || t.isOptionalMemberExpression(current)) {
|
|
4012
|
+
if ('property' in current && t.isIdentifier(current.property)) {
|
|
4013
|
+
parts.unshift(current.property.name);
|
|
4014
|
+
}
|
|
4015
|
+
if ('object' in current) {
|
|
4016
|
+
current = current.object;
|
|
4017
|
+
}
|
|
4018
|
+
else {
|
|
4019
|
+
break;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
if (t.isIdentifier(current)) {
|
|
4023
|
+
parts.unshift(current.name);
|
|
4024
|
+
}
|
|
4025
|
+
return parts.join('.');
|
|
4026
|
+
}
|
|
4027
|
+
return '...';
|
|
4028
|
+
})
|
|
4029
|
+
.filter(Boolean);
|
|
4030
|
+
violations.push({
|
|
4031
|
+
rule: 'useeffect-unstable-dependencies',
|
|
4032
|
+
severity: severity,
|
|
4033
|
+
line: dep.loc?.start.line || path.node.loc?.start.line || 0,
|
|
4034
|
+
column: dep.loc?.start.column || path.node.loc?.start.column || 0,
|
|
4035
|
+
message: message,
|
|
4036
|
+
code: `}, [${fixedDeps.join(', ')}${fixedDeps.length > 0 ? ', ' : ''}${unstableDep}]); // ${severity === 'critical' ? '🚨' : '⚠️'} Remove '${unstableDep}'`,
|
|
4037
|
+
suggestion: {
|
|
4038
|
+
text: suggestionText,
|
|
4039
|
+
example: fixedDeps.length > 0
|
|
4040
|
+
? `}, [${fixedDeps.join(', ')}]); // ✅ Removed unstable '${unstableDep}'`
|
|
4041
|
+
: `}, []); // ✅ Run once on mount - dependencies are stable`
|
|
4042
|
+
}
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
},
|
|
4048
|
+
});
|
|
4049
|
+
return violations;
|
|
4050
|
+
},
|
|
4051
|
+
},
|
|
3863
4052
|
{
|
|
3864
4053
|
name: 'server-reload-on-client-operation',
|
|
3865
4054
|
appliesTo: 'all',
|
|
@@ -4707,10 +4896,18 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4707
4896
|
providedParamsMap.set(prop.key.name.toLowerCase(), prop.key.name);
|
|
4708
4897
|
}
|
|
4709
4898
|
}
|
|
4899
|
+
// Filter to only required parameters for validation
|
|
4900
|
+
const requiredParams = specQuery.parameters.filter((p) => {
|
|
4901
|
+
const hasRequiredFlag = p.isRequired === true || p.isRequired === '1';
|
|
4902
|
+
const isRuntimeParam = p.value === '@runtime';
|
|
4903
|
+
return hasRequiredFlag || isRuntimeParam;
|
|
4904
|
+
});
|
|
4710
4905
|
const specParamNames = specQuery.parameters.map((p) => p.name);
|
|
4711
4906
|
const specParamNamesLower = specParamNames.map((n) => n.toLowerCase());
|
|
4712
|
-
// Find missing parameters (case-insensitive)
|
|
4713
|
-
const missing =
|
|
4907
|
+
// Find missing REQUIRED parameters only (case-insensitive)
|
|
4908
|
+
const missing = requiredParams
|
|
4909
|
+
.map((p) => p.name)
|
|
4910
|
+
.filter((n) => !providedParamsMap.has(n.toLowerCase()));
|
|
4714
4911
|
// Find extra parameters (not matching any spec param case-insensitively)
|
|
4715
4912
|
const extra = Array.from(providedParamsMap.values()).filter((providedName) => !specParamNamesLower.includes(providedName.toLowerCase()));
|
|
4716
4913
|
if (missing.length > 0 || extra.length > 0) {
|
|
@@ -4931,6 +5128,8 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4931
5128
|
code: suggestion || `${paramName}: <${expectedType} value>`,
|
|
4932
5129
|
});
|
|
4933
5130
|
}
|
|
5131
|
+
// NOTE: Date parameter validation has been moved to TypeInferenceEngine
|
|
5132
|
+
// and is surfaced via the 'type-inference-errors' rule
|
|
4934
5133
|
}
|
|
4935
5134
|
}
|
|
4936
5135
|
}
|
|
@@ -4940,6 +5139,36 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4940
5139
|
},
|
|
4941
5140
|
},
|
|
4942
5141
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
|
5142
|
+
// TYPE INFERENCE ERRORS RULE
|
|
5143
|
+
// Surfaces errors found by TypeInferenceEngine (e.g., date parameter validation)
|
|
5144
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
|
5145
|
+
{
|
|
5146
|
+
name: 'type-inference-errors',
|
|
5147
|
+
appliesTo: 'all',
|
|
5148
|
+
test: (ast, _componentName, componentSpec) => {
|
|
5149
|
+
const violations = [];
|
|
5150
|
+
// Create type inference engine
|
|
5151
|
+
const typeEngine = new type_inference_engine_1.TypeInferenceEngine(componentSpec);
|
|
5152
|
+
// Run analysis synchronously (validateQueryParameters is called during traversal)
|
|
5153
|
+
// The async part of analyze() is not needed for date validation
|
|
5154
|
+
typeEngine.analyze(ast);
|
|
5155
|
+
// Get errors collected during analysis
|
|
5156
|
+
const errors = typeEngine.getErrors();
|
|
5157
|
+
// Convert type inference errors to violations
|
|
5158
|
+
for (const error of errors) {
|
|
5159
|
+
violations.push({
|
|
5160
|
+
rule: 'type-inference-errors',
|
|
5161
|
+
severity: error.type === 'error' ? 'high' : 'medium',
|
|
5162
|
+
line: error.line,
|
|
5163
|
+
column: error.column,
|
|
5164
|
+
message: error.message,
|
|
5165
|
+
code: error.code || ''
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
return violations;
|
|
5169
|
+
},
|
|
5170
|
+
},
|
|
5171
|
+
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
|
4943
5172
|
// TYPE MISMATCH OPERATION RULE
|
|
4944
5173
|
// Validates that operations are type-safe (e.g., no arithmetic on strings, array methods on non-arrays)
|
|
4945
5174
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
|
@@ -9156,5 +9385,207 @@ const result = await utilities.rq.RunQuery({
|
|
|
9156
9385
|
return violations;
|
|
9157
9386
|
},
|
|
9158
9387
|
},
|
|
9388
|
+
{
|
|
9389
|
+
name: 'validate-component-props',
|
|
9390
|
+
appliesTo: 'all',
|
|
9391
|
+
test: (ast, componentName, componentSpec) => {
|
|
9392
|
+
const violations = [];
|
|
9393
|
+
// Only validate if component spec exists
|
|
9394
|
+
if (!componentSpec || !componentSpec.dependencies || componentSpec.dependencies.length === 0) {
|
|
9395
|
+
return violations;
|
|
9396
|
+
}
|
|
9397
|
+
// Build a map of dependency components to their full specs
|
|
9398
|
+
// Pattern from dependency-prop-validation rule
|
|
9399
|
+
const dependencySpecs = new Map();
|
|
9400
|
+
for (const dep of componentSpec.dependencies) {
|
|
9401
|
+
if (dep && typeof dep === 'object' && dep.name) {
|
|
9402
|
+
if (dep.location === 'registry') {
|
|
9403
|
+
let match;
|
|
9404
|
+
if (dep.registry) {
|
|
9405
|
+
match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace, dep.registry);
|
|
9406
|
+
}
|
|
9407
|
+
else {
|
|
9408
|
+
match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace);
|
|
9409
|
+
}
|
|
9410
|
+
if (match) {
|
|
9411
|
+
dependencySpecs.set(dep.name, match.spec);
|
|
9412
|
+
}
|
|
9413
|
+
}
|
|
9414
|
+
else {
|
|
9415
|
+
// Embedded dependencies have their spec inline
|
|
9416
|
+
dependencySpecs.set(dep.name, dep);
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9419
|
+
}
|
|
9420
|
+
// Build validation context helpers from parent spec's dataRequirements
|
|
9421
|
+
const getEntityFields = (entityName) => {
|
|
9422
|
+
if (!componentSpec.dataRequirements?.entities)
|
|
9423
|
+
return [];
|
|
9424
|
+
const entity = componentSpec.dataRequirements.entities.find(e => e.name === entityName);
|
|
9425
|
+
if (!entity)
|
|
9426
|
+
return [];
|
|
9427
|
+
// Prefer fieldMetadata if available (provides type info, allowsNull, isPrimaryKey, etc.)
|
|
9428
|
+
if (entity.fieldMetadata && Array.isArray(entity.fieldMetadata) && entity.fieldMetadata.length > 0) {
|
|
9429
|
+
return entity.fieldMetadata.map((f) => ({
|
|
9430
|
+
name: f.name,
|
|
9431
|
+
type: f.type || 'string',
|
|
9432
|
+
required: !f.allowsNull,
|
|
9433
|
+
allowedValues: f.possibleValues || undefined,
|
|
9434
|
+
isPrimaryKey: f.isPrimaryKey || false,
|
|
9435
|
+
}));
|
|
9436
|
+
}
|
|
9437
|
+
// Fallback: Collect all field names from display/filter/sort arrays
|
|
9438
|
+
const allFieldNames = new Set();
|
|
9439
|
+
if (entity.displayFields)
|
|
9440
|
+
entity.displayFields.forEach((f) => allFieldNames.add(f));
|
|
9441
|
+
if (entity.filterFields)
|
|
9442
|
+
entity.filterFields.forEach((f) => allFieldNames.add(f));
|
|
9443
|
+
if (entity.sortFields)
|
|
9444
|
+
entity.sortFields.forEach((f) => allFieldNames.add(f));
|
|
9445
|
+
// Convert to EntityFieldInfo format (we don't have type info from field name lists)
|
|
9446
|
+
return Array.from(allFieldNames).map(name => ({
|
|
9447
|
+
name,
|
|
9448
|
+
type: 'string', // Unknown type from field name lists
|
|
9449
|
+
required: false,
|
|
9450
|
+
allowedValues: undefined,
|
|
9451
|
+
}));
|
|
9452
|
+
};
|
|
9453
|
+
const getEntityFieldType = (entityName, fieldName) => {
|
|
9454
|
+
const fields = getEntityFields(entityName);
|
|
9455
|
+
const field = fields.find((f) => f.name === fieldName);
|
|
9456
|
+
return field?.type || null;
|
|
9457
|
+
};
|
|
9458
|
+
const hasEntity = (entityName) => {
|
|
9459
|
+
if (!componentSpec.dataRequirements?.entities)
|
|
9460
|
+
return false;
|
|
9461
|
+
return componentSpec.dataRequirements.entities.some(e => e.name === entityName);
|
|
9462
|
+
};
|
|
9463
|
+
// GENERIC validation for ALL components with constraints
|
|
9464
|
+
(0, traverse_1.default)(ast, {
|
|
9465
|
+
JSXElement(path) {
|
|
9466
|
+
const openingElement = path.node.openingElement;
|
|
9467
|
+
let elementName = '';
|
|
9468
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
9469
|
+
elementName = openingElement.name.name;
|
|
9470
|
+
}
|
|
9471
|
+
else if (t.isJSXMemberExpression(openingElement.name)) {
|
|
9472
|
+
// Handle cases like <components.EntityDataGrid> - skip for now
|
|
9473
|
+
return;
|
|
9474
|
+
}
|
|
9475
|
+
// Get the spec for this dependency component
|
|
9476
|
+
const depSpec = dependencySpecs.get(elementName);
|
|
9477
|
+
if (!depSpec || !depSpec.properties)
|
|
9478
|
+
return;
|
|
9479
|
+
// Check if this component has any properties with constraints
|
|
9480
|
+
const hasConstraints = depSpec.properties.some(p => p.constraints && p.constraints.length > 0);
|
|
9481
|
+
if (!hasConstraints)
|
|
9482
|
+
return;
|
|
9483
|
+
// Extract all props into a map for sibling prop lookups
|
|
9484
|
+
const siblingProps = new Map();
|
|
9485
|
+
for (const attr of openingElement.attributes) {
|
|
9486
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
9487
|
+
const extractedValue = prop_value_extractor_1.PropValueExtractor.extract(attr);
|
|
9488
|
+
siblingProps.set(attr.name.name, extractedValue);
|
|
9489
|
+
}
|
|
9490
|
+
}
|
|
9491
|
+
// GENERIC: Iterate through all properties with constraints
|
|
9492
|
+
for (const property of depSpec.properties) {
|
|
9493
|
+
if (!property.constraints || property.constraints.length === 0) {
|
|
9494
|
+
continue;
|
|
9495
|
+
}
|
|
9496
|
+
// Find the JSX attribute for this property
|
|
9497
|
+
const propAttr = openingElement.attributes.find((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === property.name);
|
|
9498
|
+
if (!propAttr || !t.isJSXAttribute(propAttr)) {
|
|
9499
|
+
continue;
|
|
9500
|
+
}
|
|
9501
|
+
// Extract the property value
|
|
9502
|
+
const propValue = prop_value_extractor_1.PropValueExtractor.extract(propAttr);
|
|
9503
|
+
// Skip dynamic values
|
|
9504
|
+
if (prop_value_extractor_1.PropValueExtractor.isDynamicValue(propValue)) {
|
|
9505
|
+
continue;
|
|
9506
|
+
}
|
|
9507
|
+
// Run all validators for this property's constraints
|
|
9508
|
+
for (const constraint of property.constraints) {
|
|
9509
|
+
// Use ClassFactory to instantiate validator by constraint type
|
|
9510
|
+
const validator = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(constraint_validators_1.BaseConstraintValidator, constraint.type);
|
|
9511
|
+
if (!validator) {
|
|
9512
|
+
// Validator not registered for this constraint type
|
|
9513
|
+
console.warn(`No validator registered for constraint type: ${constraint.type}`);
|
|
9514
|
+
continue;
|
|
9515
|
+
}
|
|
9516
|
+
// Build ValidationContext
|
|
9517
|
+
const context = {
|
|
9518
|
+
node: propAttr,
|
|
9519
|
+
path: path,
|
|
9520
|
+
componentName: elementName,
|
|
9521
|
+
componentSpec: depSpec,
|
|
9522
|
+
propertyName: property.name,
|
|
9523
|
+
propertyValue: propValue,
|
|
9524
|
+
siblingProps,
|
|
9525
|
+
entities: new Map(),
|
|
9526
|
+
queries: new Map(),
|
|
9527
|
+
typeEngine: null,
|
|
9528
|
+
getEntityFields,
|
|
9529
|
+
getEntityFieldType,
|
|
9530
|
+
findSimilarFieldNames: (fieldName, entityName, maxResults) => {
|
|
9531
|
+
const fields = getEntityFields(entityName);
|
|
9532
|
+
const fieldNames = fields.map((f) => f.name);
|
|
9533
|
+
const similar = [];
|
|
9534
|
+
// Simple Levenshtein distance calculation
|
|
9535
|
+
const levenshtein = (a, b) => {
|
|
9536
|
+
const matrix = [];
|
|
9537
|
+
for (let i = 0; i <= b.length; i++)
|
|
9538
|
+
matrix[i] = [i];
|
|
9539
|
+
for (let j = 0; j <= a.length; j++)
|
|
9540
|
+
matrix[0][j] = j;
|
|
9541
|
+
for (let i = 1; i <= b.length; i++) {
|
|
9542
|
+
for (let j = 1; j <= a.length; j++) {
|
|
9543
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
9544
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
9545
|
+
}
|
|
9546
|
+
else {
|
|
9547
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
9548
|
+
}
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9551
|
+
return matrix[b.length][a.length];
|
|
9552
|
+
};
|
|
9553
|
+
for (const fn of fieldNames) {
|
|
9554
|
+
const distance = levenshtein(fieldName.toLowerCase(), fn.toLowerCase());
|
|
9555
|
+
if (distance <= 3) {
|
|
9556
|
+
similar.push({ name: fn, distance });
|
|
9557
|
+
}
|
|
9558
|
+
}
|
|
9559
|
+
similar.sort((a, b) => a.distance - b.distance);
|
|
9560
|
+
return similar.slice(0, maxResults || 3).map(s => s.name);
|
|
9561
|
+
},
|
|
9562
|
+
getQueryParameters: () => [],
|
|
9563
|
+
hasQuery: () => false,
|
|
9564
|
+
hasEntity,
|
|
9565
|
+
};
|
|
9566
|
+
// Run the validator
|
|
9567
|
+
try {
|
|
9568
|
+
const constraintViolations = validator.validate(context, constraint);
|
|
9569
|
+
for (const cv of constraintViolations) {
|
|
9570
|
+
violations.push({
|
|
9571
|
+
rule: 'validate-component-props',
|
|
9572
|
+
severity: cv.severity,
|
|
9573
|
+
line: propAttr.loc?.start.line || 0,
|
|
9574
|
+
column: propAttr.loc?.start.column || 0,
|
|
9575
|
+
message: cv.message,
|
|
9576
|
+
code: cv.suggestion || '',
|
|
9577
|
+
});
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9580
|
+
catch (error) {
|
|
9581
|
+
console.error(`Error validating ${property.name} constraint (${constraint.type}):`, error);
|
|
9582
|
+
}
|
|
9583
|
+
}
|
|
9584
|
+
}
|
|
9585
|
+
},
|
|
9586
|
+
});
|
|
9587
|
+
return violations;
|
|
9588
|
+
},
|
|
9589
|
+
},
|
|
9159
9590
|
];
|
|
9160
9591
|
//# sourceMappingURL=component-linter.js.map
|