@memberjunction/react-test-harness 2.124.0 → 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.
Files changed (40) hide show
  1. package/dist/lib/component-linter.d.ts.map +1 -1
  2. package/dist/lib/component-linter.js +433 -2
  3. package/dist/lib/component-linter.js.map +1 -1
  4. package/dist/lib/constraint-validators/base-constraint-validator.d.ts +259 -0
  5. package/dist/lib/constraint-validators/base-constraint-validator.d.ts.map +1 -0
  6. package/dist/lib/constraint-validators/base-constraint-validator.js +304 -0
  7. package/dist/lib/constraint-validators/base-constraint-validator.js.map +1 -0
  8. package/dist/lib/constraint-validators/index.d.ts +21 -0
  9. package/dist/lib/constraint-validators/index.d.ts.map +1 -0
  10. package/dist/lib/constraint-validators/index.js +37 -0
  11. package/dist/lib/constraint-validators/index.js.map +1 -0
  12. package/dist/lib/constraint-validators/required-when-validator.d.ts +43 -0
  13. package/dist/lib/constraint-validators/required-when-validator.d.ts.map +1 -0
  14. package/dist/lib/constraint-validators/required-when-validator.js +97 -0
  15. package/dist/lib/constraint-validators/required-when-validator.js.map +1 -0
  16. package/dist/lib/constraint-validators/sql-where-clause-validator.d.ts +116 -0
  17. package/dist/lib/constraint-validators/sql-where-clause-validator.d.ts.map +1 -0
  18. package/dist/lib/constraint-validators/sql-where-clause-validator.js +381 -0
  19. package/dist/lib/constraint-validators/sql-where-clause-validator.js.map +1 -0
  20. package/dist/lib/constraint-validators/subset-of-entity-fields-validator.d.ts +60 -0
  21. package/dist/lib/constraint-validators/subset-of-entity-fields-validator.d.ts.map +1 -0
  22. package/dist/lib/constraint-validators/subset-of-entity-fields-validator.js +198 -0
  23. package/dist/lib/constraint-validators/subset-of-entity-fields-validator.js.map +1 -0
  24. package/dist/lib/constraint-validators/validation-context.d.ts +326 -0
  25. package/dist/lib/constraint-validators/validation-context.d.ts.map +1 -0
  26. package/dist/lib/constraint-validators/validation-context.js +14 -0
  27. package/dist/lib/constraint-validators/validation-context.js.map +1 -0
  28. package/dist/lib/prop-value-extractor.d.ts +147 -0
  29. package/dist/lib/prop-value-extractor.d.ts.map +1 -0
  30. package/dist/lib/prop-value-extractor.js +499 -0
  31. package/dist/lib/prop-value-extractor.js.map +1 -0
  32. package/dist/lib/type-context.d.ts +2 -0
  33. package/dist/lib/type-context.d.ts.map +1 -1
  34. package/dist/lib/type-context.js +22 -9
  35. package/dist/lib/type-context.js.map +1 -1
  36. package/dist/lib/type-inference-engine.d.ts +20 -0
  37. package/dist/lib/type-inference-engine.d.ts.map +1 -1
  38. package/dist/lib/type-inference-engine.js +253 -20
  39. package/dist/lib/type-inference-engine.js.map +1 -1
  40. 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;AAM/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,CAm4OpC;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"}
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 = specParamNames.filter((n) => !providedParamsMap.has(n.toLowerCase()));
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