@memberjunction/react-test-harness 2.98.0 → 2.100.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.
@@ -1584,34 +1584,24 @@ await utilities.rq.RunQuery({
1584
1584
  break;
1585
1585
  case 'component-props-validation':
1586
1586
  violation.suggestion = {
1587
- text: 'Components can only accept standard props and props explicitly defined in the component spec. Additional props must be declared in the spec\'s properties array.',
1587
+ text: 'Components can only accept standard props and props explicitly defined in the component spec. The spec is provided by the architect and cannot be modified - your code must match the spec exactly.',
1588
1588
  example: `// ❌ WRONG - Component with undeclared props:
1589
1589
  function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
1590
- // customers, orders, selectedId are NOT allowed unless defined in spec
1590
+ // ERROR: customers, orders, selectedId are NOT in the spec
1591
+ // The spec defines what props are allowed - you cannot add new ones
1591
1592
  }
1592
1593
 
1593
- // ✅ CORRECT Option 1 - Use only standard props and load data internally:
1594
+ // ✅ CORRECT - Use only standard props and props defined in the spec:
1594
1595
  function MyComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
1595
- // Load data internally using utilities
1596
+ // If you need data like customers/orders, load it internally using utilities
1596
1597
  const [customers, setCustomers] = useState([]);
1597
1598
  const [orders, setOrders] = useState([]);
1598
1599
  const [selectedId, setSelectedId] = useState(savedUserSettings?.selectedId);
1599
- }
1600
-
1601
- // ✅ CORRECT Option 2 - Define props in component spec:
1602
- // In spec.properties array:
1603
- // [
1604
- // { name: "customers", type: "array", required: false, description: "Customer list" },
1605
- // { name: "orders", type: "array", required: false, description: "Order list" },
1606
- // { name: "selectedId", type: "string", required: false, description: "Selected item ID" }
1607
- // ]
1608
- // Then the component can accept them:
1609
- function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
1610
- // These props are now allowed because they're defined in the spec
1611
1600
 
1612
1601
  useEffect(() => {
1613
1602
  const loadData = async () => {
1614
1603
  try {
1604
+ // Load customers data internally
1615
1605
  const result = await utilities.rv.RunView({
1616
1606
  EntityName: 'Customers',
1617
1607
  Fields: ['ID', 'Name', 'Status']
@@ -1626,8 +1616,12 @@ function MyComponent({ utilities, styles, components, customers, orders, selecte
1626
1616
  loadData();
1627
1617
  }, []);
1628
1618
 
1629
- return <div>{/* Use state, not props */}</div>;
1630
- }`
1619
+ return <div>{/* Use state variables, not props */}</div>;
1620
+ }
1621
+
1622
+ // NOTE: If the spec DOES define additional props (e.g., customers, orders),
1623
+ // then you MUST accept and use them. Check the spec's properties array
1624
+ // to see what props are required/optional beyond the standard ones.`
1631
1625
  };
1632
1626
  break;
1633
1627
  case 'runview-runquery-result-direct-usage':
@@ -2678,7 +2672,7 @@ ComponentLinter.universalComponentRules = [
2678
2672
  severity: 'critical',
2679
2673
  line: path.node.loc?.start.line || 0,
2680
2674
  column: path.node.loc?.start.column || 0,
2681
- message: `Component "${componentName}" is trying to destructure from window.${propertyName}. If this is a library, it should be added to the component's libraries array in the spec and accessed via its globalVariable name.`,
2675
+ message: `Component "${componentName}" is trying to access window.${propertyName}. Libraries must be accessed using unwrapComponents, not through the window object. If this library is in your spec, use: const { ... } = unwrapComponents(${propertyName}, [...]); If it's not in your spec, you cannot use it.`,
2682
2676
  code: path.toString().substring(0, 100)
2683
2677
  });
2684
2678
  }
@@ -2916,6 +2910,66 @@ ComponentLinter.universalComponentRules = [
2916
2910
  return violations;
2917
2911
  }
2918
2912
  },
2913
+ {
2914
+ name: 'use-unwrap-components',
2915
+ appliesTo: 'all',
2916
+ test: (ast, componentName, componentSpec) => {
2917
+ const violations = [];
2918
+ // Build a set of library global variables
2919
+ const libraryGlobals = new Set();
2920
+ if (componentSpec?.libraries) {
2921
+ for (const lib of componentSpec.libraries) {
2922
+ if (lib.globalVariable) {
2923
+ libraryGlobals.add(lib.globalVariable);
2924
+ }
2925
+ }
2926
+ }
2927
+ (0, traverse_1.default)(ast, {
2928
+ VariableDeclarator(path) {
2929
+ // Check for direct destructuring from library globals
2930
+ if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
2931
+ const sourceVar = path.node.init.name;
2932
+ // Check if this is destructuring from a library global
2933
+ if (libraryGlobals.has(sourceVar)) {
2934
+ // Extract the destructured component names
2935
+ const componentNames = [];
2936
+ for (const prop of path.node.id.properties) {
2937
+ if (t.isObjectProperty(prop)) {
2938
+ if (t.isIdentifier(prop.key)) {
2939
+ componentNames.push(prop.key.name);
2940
+ }
2941
+ }
2942
+ }
2943
+ violations.push({
2944
+ rule: 'use-unwrap-components',
2945
+ severity: 'critical',
2946
+ line: path.node.loc?.start.line || 0,
2947
+ column: path.node.loc?.start.column || 0,
2948
+ message: `Direct destructuring from library "${sourceVar}" is not allowed. You MUST use unwrapComponents to access library components. Replace "const { ${componentNames.join(', ')} } = ${sourceVar};" with "const { ${componentNames.join(', ')} } = unwrapComponents(${sourceVar}, [${componentNames.map(n => `'${n}'`).join(', ')}]);"`
2949
+ });
2950
+ }
2951
+ }
2952
+ // Also check for MemberExpression destructuring like const { Button } = antd.Button
2953
+ if (t.isObjectPattern(path.node.id) && t.isMemberExpression(path.node.init)) {
2954
+ const memberExpr = path.node.init;
2955
+ if (t.isIdentifier(memberExpr.object)) {
2956
+ const objName = memberExpr.object.name;
2957
+ if (libraryGlobals.has(objName)) {
2958
+ violations.push({
2959
+ rule: 'use-unwrap-components',
2960
+ severity: 'critical',
2961
+ line: path.node.loc?.start.line || 0,
2962
+ column: path.node.loc?.start.column || 0,
2963
+ message: `Direct destructuring from library member expression is not allowed. Use unwrapComponents to safely access library components. Example: Instead of "const { Something } = ${objName}.Something;", use "const { Something } = unwrapComponents(${objName}, ['Something']);"`
2964
+ });
2965
+ }
2966
+ }
2967
+ }
2968
+ }
2969
+ });
2970
+ return violations;
2971
+ }
2972
+ },
2919
2973
  {
2920
2974
  name: 'library-variable-names',
2921
2975
  appliesTo: 'all',
@@ -2948,7 +3002,7 @@ ComponentLinter.universalComponentRules = [
2948
3002
  severity: 'critical',
2949
3003
  line: path.node.loc?.start.line || 0,
2950
3004
  column: path.node.loc?.start.column || 0,
2951
- message: `Incorrect library global variable "${sourceVar}". Use the exact globalVariable from the library spec: "${correctGlobal}". Change "const { ... } = ${sourceVar};" to "const { ... } = ${correctGlobal};"`
3005
+ message: `Incorrect library global variable "${sourceVar}". Use unwrapComponents with the correct global: "const { ... } = unwrapComponents(${correctGlobal}, [...]);"`
2952
3006
  });
2953
3007
  }
2954
3008
  }
@@ -4962,6 +5016,92 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4962
5016
  return violations;
4963
5017
  }
4964
5018
  },
5019
+ {
5020
+ name: 'string-replace-all-occurrences',
5021
+ appliesTo: 'all',
5022
+ test: (ast, componentName, componentSpec) => {
5023
+ const violations = [];
5024
+ // Template patterns that are HIGH severity (likely to have multiple occurrences)
5025
+ const templatePatterns = [
5026
+ { pattern: /\{\{[^}]+\}\}/, example: '{{field}}', desc: 'double curly braces' },
5027
+ { pattern: /\{[^}]+\}/, example: '{field}', desc: 'single curly braces' },
5028
+ { pattern: /<<[^>]+>>/, example: '<<field>>', desc: 'double angle brackets' },
5029
+ { pattern: /<[^>]+>/, example: '<field>', desc: 'single angle brackets' }
5030
+ ];
5031
+ (0, traverse_1.default)(ast, {
5032
+ CallExpression(path) {
5033
+ const callee = path.node.callee;
5034
+ // Check if it's a .replace() method call
5035
+ if (t.isMemberExpression(callee) &&
5036
+ t.isIdentifier(callee.property) &&
5037
+ callee.property.name === 'replace') {
5038
+ const args = path.node.arguments;
5039
+ if (args.length >= 2) {
5040
+ const [searchArg, replaceArg] = args;
5041
+ // Handle string literal search patterns
5042
+ if (t.isStringLiteral(searchArg)) {
5043
+ const searchValue = searchArg.value;
5044
+ // Check if it matches any template pattern
5045
+ let matchedPattern = null;
5046
+ for (const tp of templatePatterns) {
5047
+ if (tp.pattern.test(searchValue)) {
5048
+ matchedPattern = tp;
5049
+ break;
5050
+ }
5051
+ }
5052
+ if (matchedPattern) {
5053
+ // HIGH severity for template patterns
5054
+ violations.push({
5055
+ rule: 'string-replace-all-occurrences',
5056
+ severity: 'high',
5057
+ line: path.node.loc?.start.line || 0,
5058
+ column: path.node.loc?.start.column || 0,
5059
+ message: `Using replace() with ${matchedPattern.desc} template '${searchValue}' only replaces the first occurrence. This will cause bugs if the template appears multiple times.`,
5060
+ suggestion: {
5061
+ text: `Use .replaceAll('${searchValue}', ...) to replace all occurrences`,
5062
+ example: `str.replaceAll('${searchValue}', value)`
5063
+ }
5064
+ });
5065
+ }
5066
+ else {
5067
+ // LOW severity for general replace() usage
5068
+ violations.push({
5069
+ rule: 'string-replace-all-occurrences',
5070
+ severity: 'low',
5071
+ line: path.node.loc?.start.line || 0,
5072
+ column: path.node.loc?.start.column || 0,
5073
+ message: `Note: replace() only replaces the first occurrence of '${searchValue}'. If you need to replace all occurrences, use replaceAll() or a global regex.`,
5074
+ suggestion: {
5075
+ text: `Consider if you need replaceAll() instead`,
5076
+ example: `str.replaceAll('${searchValue}', value) or str.replace(/${searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/g, value)`
5077
+ }
5078
+ });
5079
+ }
5080
+ }
5081
+ // Handle regex patterns - only warn if not global
5082
+ else if (t.isRegExpLiteral(searchArg)) {
5083
+ const flags = searchArg.flags || '';
5084
+ if (!flags.includes('g')) {
5085
+ violations.push({
5086
+ rule: 'string-replace-all-occurrences',
5087
+ severity: 'low',
5088
+ line: path.node.loc?.start.line || 0,
5089
+ column: path.node.loc?.start.column || 0,
5090
+ message: `Regex pattern without 'g' flag only replaces first match. Add 'g' flag for global replacement.`,
5091
+ suggestion: {
5092
+ text: `Add 'g' flag to replace all matches`,
5093
+ example: `str.replace(/${searchArg.pattern}/${flags}g, value)`
5094
+ }
5095
+ });
5096
+ }
5097
+ }
5098
+ }
5099
+ }
5100
+ }
5101
+ });
5102
+ return violations;
5103
+ }
5104
+ },
4965
5105
  {
4966
5106
  name: 'component-props-validation',
4967
5107
  appliesTo: 'all',
@@ -4970,8 +5110,10 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4970
5110
  const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
4971
5111
  // React special props that are automatically provided by React
4972
5112
  const reactSpecialProps = new Set(['children']);
4973
- // Build set of allowed props: standard props + React special props + componentSpec properties
5113
+ // Build set of allowed props: standard props + React special props + componentSpec properties + events
4974
5114
  const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
5115
+ // Track required props separately for validation
5116
+ const requiredProps = new Set();
4975
5117
  // Add props from componentSpec.properties if they exist
4976
5118
  // These are the architect-defined props that this component is allowed to accept
4977
5119
  const specDefinedProps = [];
@@ -4980,6 +5122,21 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4980
5122
  if (prop.name) {
4981
5123
  allowedProps.add(prop.name);
4982
5124
  specDefinedProps.push(prop.name);
5125
+ if (prop.required) {
5126
+ requiredProps.add(prop.name);
5127
+ }
5128
+ }
5129
+ }
5130
+ }
5131
+ // Add events from componentSpec.events if they exist
5132
+ // Events are functions passed as props to the component
5133
+ const specDefinedEvents = [];
5134
+ if (componentSpec?.events) {
5135
+ for (const event of componentSpec.events) {
5136
+ if (event.name) {
5137
+ allowedProps.add(event.name);
5138
+ specDefinedEvents.push(event.name);
5139
+ // Events are typically optional unless explicitly marked required
4983
5140
  }
4984
5141
  }
4985
5142
  }
@@ -4999,23 +5156,36 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4999
5156
  }
5000
5157
  }
5001
5158
  }
5159
+ // Check for missing required props
5160
+ const missingRequired = Array.from(requiredProps).filter(prop => !allProps.includes(prop) && !standardProps.has(prop));
5161
+ // Report missing required props
5162
+ if (missingRequired.length > 0) {
5163
+ violations.push({
5164
+ rule: 'component-props-validation',
5165
+ severity: 'critical',
5166
+ line: path.node.loc?.start.line || 0,
5167
+ column: path.node.loc?.start.column || 0,
5168
+ message: `Component "${componentName}" is missing required props: ${missingRequired.join(', ')}. These props are marked as required in the component specification.`
5169
+ });
5170
+ }
5002
5171
  // Only report if there are non-allowed props
5003
5172
  if (invalidProps.length > 0) {
5004
5173
  let message;
5005
- if (specDefinedProps.length > 0) {
5174
+ if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
5006
5175
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5007
5176
  `This component can only accept: ` +
5008
5177
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5009
- `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
5010
- `(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5011
- `Any additional props must be defined in the component spec's properties array.`;
5178
+ (specDefinedProps.length > 0 ? `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` : '') +
5179
+ (specDefinedEvents.length > 0 ? `(3) Spec-defined events: ${specDefinedEvents.join(', ')}, ` : '') +
5180
+ `(4) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5181
+ `Any additional props must be defined in the component spec's properties or events array.`;
5012
5182
  }
5013
5183
  else {
5014
5184
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5015
5185
  `This component can only accept: ` +
5016
5186
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5017
5187
  `(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5018
- `To accept additional props, they must be defined in the component spec's properties array.`;
5188
+ `To accept additional props, they must be defined in the component spec's properties or events array.`;
5019
5189
  }
5020
5190
  violations.push({
5021
5191
  rule: 'component-props-validation',
@@ -5046,28 +5216,41 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5046
5216
  }
5047
5217
  }
5048
5218
  }
5219
+ // Check for missing required props
5220
+ const missingRequired = Array.from(requiredProps).filter(prop => !allProps.includes(prop) && !standardProps.has(prop));
5221
+ // Report missing required props
5222
+ if (missingRequired.length > 0) {
5223
+ violations.push({
5224
+ rule: 'component-props-validation',
5225
+ severity: 'critical',
5226
+ line: init.loc?.start.line || 0,
5227
+ column: init.loc?.start.column || 0,
5228
+ message: `Component "${componentName}" is missing required props: ${missingRequired.join(', ')}. These props are marked as required in the component specification.`
5229
+ });
5230
+ }
5049
5231
  if (invalidProps.length > 0) {
5050
5232
  let message;
5051
- if (specDefinedProps.length > 0) {
5233
+ if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
5052
5234
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5053
5235
  `This component can only accept: ` +
5054
5236
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5055
- `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
5056
- `(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5057
- `Any additional props must be defined in the component spec's properties array.`;
5237
+ (specDefinedProps.length > 0 ? `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` : '') +
5238
+ (specDefinedEvents.length > 0 ? `(3) Spec-defined events: ${specDefinedEvents.join(', ')}, ` : '') +
5239
+ `(4) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5240
+ `Any additional props must be defined in the component spec's properties or events array.`;
5058
5241
  }
5059
5242
  else {
5060
5243
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5061
5244
  `This component can only accept: ` +
5062
5245
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5063
5246
  `(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5064
- `To accept additional props, they must be defined in the component spec's properties array.`;
5247
+ `To accept additional props, they must be defined in the component spec's properties or events array.`;
5065
5248
  }
5066
5249
  violations.push({
5067
5250
  rule: 'component-props-validation',
5068
5251
  severity: 'critical',
5069
- line: path.node.loc?.start.line || 0,
5070
- column: path.node.loc?.start.column || 0,
5252
+ line: init.loc?.start.line || 0,
5253
+ column: init.loc?.start.column || 0,
5071
5254
  message
5072
5255
  });
5073
5256
  }
@@ -5668,7 +5851,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5668
5851
  severity: 'critical',
5669
5852
  line: openingElement.loc?.start.line || 0,
5670
5853
  column: openingElement.loc?.start.column || 0,
5671
- message: `JSX component "${tagName}" is not defined. This looks like it should be destructured from the ${libraryNames[0]} library. Add: const { ${tagName} } = ${libraryNames[0]}; at the top of your component function.`,
5854
+ message: `JSX component "${tagName}" is not defined. This looks like it should be from the ${libraryNames[0]} library. Add: const { ${tagName} } = unwrapComponents(${libraryNames[0]}, ['${tagName}']); at the top of your component function.`,
5672
5855
  code: `<${tagName} ... />`
5673
5856
  });
5674
5857
  } else {
@@ -5678,7 +5861,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5678
5861
  severity: 'critical',
5679
5862
  line: openingElement.loc?.start.line || 0,
5680
5863
  column: openingElement.loc?.start.column || 0,
5681
- message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}. Destructure it from the appropriate library, e.g., const { ${tagName} } = LibraryName;`,
5864
+ message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}. Use unwrapComponents to access it: const { ${tagName} } = unwrapComponents(LibraryName, ['${tagName}']);`,
5682
5865
  code: `<${tagName} ... />`
5683
5866
  });
5684
5867
  }
@@ -5711,7 +5894,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5711
5894
  severity: 'high',
5712
5895
  line: openingElement.loc?.start.line || 0,
5713
5896
  column: openingElement.loc?.start.column || 0,
5714
- message: `JSX component "${tagName}" is not defined. Either define it in your component, add it to dependencies, or check if it should be destructured from a library.`,
5897
+ message: `JSX component "${tagName}" is not defined. You must either: (1) define it in your component, (2) use a component that's already in the spec's dependencies, or (3) destructure it from a library that's already in the spec's libraries.`,
5715
5898
  code: `<${tagName} ... />`
5716
5899
  });
5717
5900
  }
@@ -5739,42 +5922,13 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5739
5922
  appliesTo: 'all',
5740
5923
  test: (ast, componentName, componentSpec) => {
5741
5924
  const violations = [];
5742
- // Extract declared queries and entities from dataRequirements
5743
- const declaredQueries = new Set();
5744
- const declaredEntities = new Set();
5745
- if (componentSpec?.dataRequirements) {
5746
- // Handle queries in different possible locations
5747
- if (Array.isArray(componentSpec.dataRequirements)) {
5748
- // If it's an array directly
5749
- componentSpec.dataRequirements.forEach((req) => {
5750
- if (req.type === 'query' && req.name) {
5751
- declaredQueries.add(req.name.toLowerCase());
5752
- }
5753
- if (req.type === 'entity' && req.name) {
5754
- declaredEntities.add(req.name.toLowerCase());
5755
- }
5756
- });
5757
- }
5758
- else if (typeof componentSpec.dataRequirements === 'object') {
5759
- // If it's an object with queries/entities properties
5760
- if (componentSpec.dataRequirements.queries) {
5761
- componentSpec.dataRequirements.queries.forEach((q) => {
5762
- if (q.name)
5763
- declaredQueries.add(q.name.toLowerCase());
5764
- });
5765
- }
5766
- if (componentSpec.dataRequirements.entities) {
5767
- componentSpec.dataRequirements.entities.forEach((e) => {
5768
- if (e.name)
5769
- declaredEntities.add(e.name.toLowerCase());
5770
- });
5771
- }
5772
- }
5773
- }
5925
+ // NOTE: Entity/Query name validation removed from this rule to avoid duplication
5926
+ // The 'data-requirements-validation' rule handles comprehensive entity/query validation
5927
+ // This rule now focuses on RunQuery/RunView specific issues like SQL injection
5774
5928
  (0, traverse_1.default)(ast, {
5775
5929
  CallExpression(path) {
5776
5930
  const callee = path.node.callee;
5777
- // Check for RunQuery calls
5931
+ // Check for RunQuery calls - focus on SQL injection detection
5778
5932
  if (t.isMemberExpression(callee) &&
5779
5933
  t.isIdentifier(callee.property) &&
5780
5934
  callee.property.name === 'RunQuery') {
@@ -5804,20 +5958,9 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5804
5958
  code: value.value.substring(0, 100)
5805
5959
  });
5806
5960
  }
5807
- else if (declaredQueries.size > 0 && !declaredQueries.has(queryName.toLowerCase())) {
5808
- // Only validate if we have declared queries
5809
- violations.push({
5810
- rule: 'runquery-runview-validation',
5811
- severity: 'high',
5812
- line: value.loc?.start.line || 0,
5813
- column: value.loc?.start.column || 0,
5814
- message: `Query "${queryName}" is not declared in dataRequirements.queries. Available queries: ${Array.from(declaredQueries).join(', ')}`,
5815
- code: path.toString().substring(0, 100)
5816
- });
5817
- }
5818
5961
  }
5819
5962
  else if (t.isIdentifier(value) || t.isTemplateLiteral(value)) {
5820
- // Dynamic query name - check if it might be SQL
5963
+ // Dynamic query name - warn that it shouldn't be SQL
5821
5964
  violations.push({
5822
5965
  rule: 'runquery-runview-validation',
5823
5966
  severity: 'medium',
@@ -5830,43 +5973,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5830
5973
  }
5831
5974
  }
5832
5975
  }
5833
- // Check for RunView calls
5834
- if (t.isMemberExpression(callee) &&
5835
- t.isIdentifier(callee.property) &&
5836
- (callee.property.name === 'RunView' || callee.property.name === 'RunViews')) {
5837
- const args = path.node.arguments;
5838
- // Handle both single object and array of objects
5839
- const checkEntityName = (objExpr) => {
5840
- const entityNameProp = objExpr.properties.find(p => t.isObjectProperty(p) &&
5841
- t.isIdentifier(p.key) &&
5842
- p.key.name === 'EntityName');
5843
- if (entityNameProp && t.isObjectProperty(entityNameProp) && t.isStringLiteral(entityNameProp.value)) {
5844
- const entityName = entityNameProp.value.value;
5845
- if (declaredEntities.size > 0 && !declaredEntities.has(entityName.toLowerCase())) {
5846
- violations.push({
5847
- rule: 'runquery-runview-validation',
5848
- severity: 'high',
5849
- line: entityNameProp.value.loc?.start.line || 0,
5850
- column: entityNameProp.value.loc?.start.column || 0,
5851
- message: `Entity "${entityName}" is not declared in dataRequirements.entities. Available entities: ${Array.from(declaredEntities).join(', ')}`,
5852
- code: path.toString().substring(0, 100)
5853
- });
5854
- }
5855
- }
5856
- };
5857
- if (args.length > 0) {
5858
- if (t.isObjectExpression(args[0])) {
5859
- checkEntityName(args[0]);
5860
- }
5861
- else if (t.isArrayExpression(args[0])) {
5862
- args[0].elements.forEach(elem => {
5863
- if (t.isObjectExpression(elem)) {
5864
- checkEntityName(elem);
5865
- }
5866
- });
5867
- }
5868
- }
5869
- }
5976
+ // RunView validation removed - handled by data-requirements-validation
5870
5977
  }
5871
5978
  });
5872
5979
  return violations;
@@ -8096,6 +8203,455 @@ const [state, setState] = useState(initialValue);`
8096
8203
  });
8097
8204
  return violations;
8098
8205
  }
8206
+ },
8207
+ {
8208
+ name: 'callbacks-usage-validation',
8209
+ appliesTo: 'all',
8210
+ test: (ast, componentName, componentSpec) => {
8211
+ const violations = [];
8212
+ // Define the allowed methods on ComponentCallbacks interface
8213
+ const allowedCallbackMethods = new Set(['OpenEntityRecord', 'RegisterMethod']);
8214
+ // Build list of component's event names from spec
8215
+ const componentEvents = new Set();
8216
+ if (componentSpec?.events) {
8217
+ for (const event of componentSpec.events) {
8218
+ if (event.name) {
8219
+ componentEvents.add(event.name);
8220
+ }
8221
+ }
8222
+ }
8223
+ (0, traverse_1.default)(ast, {
8224
+ MemberExpression(path) {
8225
+ // Check for callbacks.something access
8226
+ if (t.isIdentifier(path.node.object) && path.node.object.name === 'callbacks') {
8227
+ if (t.isIdentifier(path.node.property)) {
8228
+ const methodName = path.node.property.name;
8229
+ // Check if it's trying to access an event
8230
+ if (componentEvents.has(methodName)) {
8231
+ violations.push({
8232
+ rule: 'callbacks-usage-validation',
8233
+ severity: 'critical',
8234
+ line: path.node.loc?.start.line || 0,
8235
+ column: path.node.loc?.start.column || 0,
8236
+ message: `Event "${methodName}" should not be accessed from callbacks. Events are passed as direct props to the component. Use the prop directly: ${methodName}`,
8237
+ suggestion: {
8238
+ text: `Events defined in the component spec are passed as direct props, not through callbacks. Access the event directly as a prop.`,
8239
+ example: `// ❌ WRONG - Accessing event from callbacks
8240
+ const { ${methodName} } = callbacks || {};
8241
+ callbacks?.${methodName}?.(data);
8242
+
8243
+ // ✅ CORRECT - Event is a direct prop
8244
+ // In the component props destructuring:
8245
+ function MyComponent({ ..., ${methodName} }) {
8246
+ // Use with null checking:
8247
+ if (${methodName}) {
8248
+ ${methodName}(data);
8249
+ }
8250
+ }`
8251
+ }
8252
+ });
8253
+ }
8254
+ else if (!allowedCallbackMethods.has(methodName)) {
8255
+ // It's not an allowed callback method
8256
+ violations.push({
8257
+ rule: 'callbacks-usage-validation',
8258
+ severity: 'critical',
8259
+ line: path.node.loc?.start.line || 0,
8260
+ column: path.node.loc?.start.column || 0,
8261
+ message: `Invalid callback method "${methodName}". The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
8262
+ suggestion: {
8263
+ text: `The callbacks prop is reserved for specific MemberJunction framework methods. Custom events should be defined in the component spec's events array and passed as props.`,
8264
+ example: `// Allowed callbacks methods:
8265
+ callbacks?.OpenEntityRecord?.(entityName, key);
8266
+ callbacks?.RegisterMethod?.(methodName, handler);
8267
+
8268
+ // For custom events, define them in the spec and use as props:
8269
+ function MyComponent({ onCustomEvent }) {
8270
+ if (onCustomEvent) {
8271
+ onCustomEvent(data);
8272
+ }
8273
+ }`
8274
+ }
8275
+ });
8276
+ }
8277
+ }
8278
+ }
8279
+ },
8280
+ // Also check for destructuring from callbacks
8281
+ VariableDeclarator(path) {
8282
+ if (t.isObjectPattern(path.node.id) &&
8283
+ t.isIdentifier(path.node.init) &&
8284
+ path.node.init.name === 'callbacks') {
8285
+ // Check each destructured property
8286
+ for (const prop of path.node.id.properties) {
8287
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
8288
+ const methodName = prop.key.name;
8289
+ if (componentEvents.has(methodName)) {
8290
+ violations.push({
8291
+ rule: 'callbacks-usage-validation',
8292
+ severity: 'critical',
8293
+ line: prop.loc?.start.line || 0,
8294
+ column: prop.loc?.start.column || 0,
8295
+ message: `Event "${methodName}" should not be destructured from callbacks. Events are passed as direct props to the component.`,
8296
+ suggestion: {
8297
+ text: `Events should be destructured from the component props, not from callbacks.`,
8298
+ example: `// ❌ WRONG
8299
+ const { ${methodName} } = callbacks || {};
8300
+
8301
+ // ✅ CORRECT
8302
+ function MyComponent({ utilities, styles, callbacks, ${methodName} }) {
8303
+ // ${methodName} is now available as a prop
8304
+ }`
8305
+ }
8306
+ });
8307
+ }
8308
+ else if (!allowedCallbackMethods.has(methodName)) {
8309
+ violations.push({
8310
+ rule: 'callbacks-usage-validation',
8311
+ severity: 'critical',
8312
+ line: prop.loc?.start.line || 0,
8313
+ column: prop.loc?.start.column || 0,
8314
+ message: `Invalid callback method "${methodName}" being destructured. The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
8315
+ });
8316
+ }
8317
+ }
8318
+ }
8319
+ }
8320
+ // Also check for: const { something } = callbacks || {}
8321
+ if (t.isObjectPattern(path.node.id) &&
8322
+ t.isLogicalExpression(path.node.init) &&
8323
+ path.node.init.operator === '||' &&
8324
+ t.isIdentifier(path.node.init.left) &&
8325
+ path.node.init.left.name === 'callbacks') {
8326
+ // Check each destructured property
8327
+ for (const prop of path.node.id.properties) {
8328
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
8329
+ const methodName = prop.key.name;
8330
+ if (componentEvents.has(methodName)) {
8331
+ violations.push({
8332
+ rule: 'callbacks-usage-validation',
8333
+ severity: 'critical',
8334
+ line: prop.loc?.start.line || 0,
8335
+ column: prop.loc?.start.column || 0,
8336
+ message: `Event "${methodName}" should not be destructured from callbacks. Events are passed as direct props to the component.`,
8337
+ suggestion: {
8338
+ text: `Events should be destructured from the component props, not from callbacks.`,
8339
+ example: `// ❌ WRONG
8340
+ const { ${methodName} } = callbacks || {};
8341
+
8342
+ // ✅ CORRECT
8343
+ function MyComponent({ utilities, styles, callbacks, ${methodName} }) {
8344
+ // ${methodName} is now available as a prop
8345
+ }`
8346
+ }
8347
+ });
8348
+ }
8349
+ else if (!allowedCallbackMethods.has(methodName)) {
8350
+ violations.push({
8351
+ rule: 'callbacks-usage-validation',
8352
+ severity: 'critical',
8353
+ line: prop.loc?.start.line || 0,
8354
+ column: prop.loc?.start.column || 0,
8355
+ message: `Invalid callback method "${methodName}" being destructured. The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
8356
+ });
8357
+ }
8358
+ }
8359
+ }
8360
+ }
8361
+ }
8362
+ });
8363
+ return violations;
8364
+ }
8365
+ },
8366
+ {
8367
+ name: 'event-invocation-pattern',
8368
+ appliesTo: 'all',
8369
+ test: (ast, componentName, componentSpec) => {
8370
+ const violations = [];
8371
+ // Build list of component's event names from spec
8372
+ const componentEvents = new Set();
8373
+ if (componentSpec?.events) {
8374
+ for (const event of componentSpec.events) {
8375
+ if (event.name) {
8376
+ componentEvents.add(event.name);
8377
+ }
8378
+ }
8379
+ }
8380
+ // If no events defined, skip this rule
8381
+ if (componentEvents.size === 0) {
8382
+ return violations;
8383
+ }
8384
+ (0, traverse_1.default)(ast, {
8385
+ CallExpression(path) {
8386
+ // Check if calling an event without null checking
8387
+ if (t.isIdentifier(path.node.callee)) {
8388
+ const eventName = path.node.callee.name;
8389
+ if (componentEvents.has(eventName)) {
8390
+ // Check if this call is inside a conditional that checks for the event
8391
+ let hasNullCheck = false;
8392
+ let currentPath = path.parentPath;
8393
+ // Walk up the tree to see if we're inside an if statement that checks this event
8394
+ while (currentPath && !hasNullCheck) {
8395
+ if (t.isIfStatement(currentPath.node)) {
8396
+ const test = currentPath.node.test;
8397
+ // Check if the test checks for the event (simple cases)
8398
+ if (t.isIdentifier(test) && test.name === eventName) {
8399
+ hasNullCheck = true;
8400
+ }
8401
+ else if (t.isLogicalExpression(test) && test.operator === '&&') {
8402
+ // Check for patterns like: eventName && ...
8403
+ if (t.isIdentifier(test.left) && test.left.name === eventName) {
8404
+ hasNullCheck = true;
8405
+ }
8406
+ }
8407
+ }
8408
+ else if (t.isLogicalExpression(currentPath.node) && currentPath.node.operator === '&&') {
8409
+ // Check for inline conditional: eventName && eventName()
8410
+ if (t.isIdentifier(currentPath.node.left) && currentPath.node.left.name === eventName) {
8411
+ hasNullCheck = true;
8412
+ }
8413
+ }
8414
+ else if (t.isConditionalExpression(currentPath.node)) {
8415
+ // Check for ternary: eventName ? eventName() : null
8416
+ if (t.isIdentifier(currentPath.node.test) && currentPath.node.test.name === eventName) {
8417
+ hasNullCheck = true;
8418
+ }
8419
+ }
8420
+ currentPath = currentPath.parentPath || null;
8421
+ }
8422
+ if (!hasNullCheck) {
8423
+ violations.push({
8424
+ rule: 'event-invocation-pattern',
8425
+ severity: 'medium',
8426
+ line: path.node.loc?.start.line || 0,
8427
+ column: path.node.loc?.start.column || 0,
8428
+ message: `Event "${eventName}" is being invoked without null-checking. Events are optional props and should be checked before invocation.`,
8429
+ suggestion: {
8430
+ text: `Always check that an event prop exists before invoking it, as events are optional.`,
8431
+ example: `// ❌ WRONG - No null check
8432
+ ${eventName}(data);
8433
+
8434
+ // ✅ CORRECT - With null check
8435
+ if (${eventName}) {
8436
+ ${eventName}(data);
8437
+ }
8438
+
8439
+ // ✅ ALSO CORRECT - Inline check
8440
+ ${eventName} && ${eventName}(data);
8441
+
8442
+ // ✅ ALSO CORRECT - Optional chaining
8443
+ ${eventName}?.(data);`
8444
+ }
8445
+ });
8446
+ }
8447
+ }
8448
+ }
8449
+ },
8450
+ // Check for optional chaining on events (this is good!)
8451
+ OptionalCallExpression(path) {
8452
+ if (t.isIdentifier(path.node.callee)) {
8453
+ const eventName = path.node.callee.name;
8454
+ if (componentEvents.has(eventName)) {
8455
+ // This is actually the correct pattern, no violation
8456
+ return;
8457
+ }
8458
+ }
8459
+ }
8460
+ });
8461
+ return violations;
8462
+ }
8463
+ },
8464
+ {
8465
+ name: 'callbacks-passthrough-only',
8466
+ appliesTo: 'all',
8467
+ test: (ast, _componentName, _componentSpec) => {
8468
+ const violations = [];
8469
+ (0, traverse_1.default)(ast, {
8470
+ JSXAttribute(path) {
8471
+ // Check if this is a callbacks prop being passed to a component
8472
+ if (t.isJSXIdentifier(path.node.name) && path.node.name.name === 'callbacks') {
8473
+ const value = path.node.value;
8474
+ // Check if value is a JSXExpressionContainer
8475
+ if (t.isJSXExpressionContainer(value)) {
8476
+ const expr = value.expression;
8477
+ // Valid patterns:
8478
+ // - callbacks={callbacks}
8479
+ // - callbacks={props.callbacks}
8480
+ // - callbacks={restProps.callbacks}
8481
+ const isValidPassthrough = (t.isIdentifier(expr) && expr.name === 'callbacks') ||
8482
+ (t.isMemberExpression(expr) &&
8483
+ t.isIdentifier(expr.property) &&
8484
+ expr.property.name === 'callbacks');
8485
+ if (!isValidPassthrough) {
8486
+ // Check for spreading pattern: {...callbacks, ...}
8487
+ if (t.isObjectExpression(expr)) {
8488
+ const hasSpread = expr.properties.some(prop => t.isSpreadElement(prop) &&
8489
+ t.isIdentifier(prop.argument) &&
8490
+ prop.argument.name === 'callbacks');
8491
+ if (hasSpread) {
8492
+ // Found spreading callbacks with additional properties
8493
+ const addedProps = expr.properties
8494
+ .filter(prop => !t.isSpreadElement(prop) && t.isObjectProperty(prop))
8495
+ .map(prop => {
8496
+ if (t.isObjectProperty(prop)) {
8497
+ if (t.isIdentifier(prop.key)) {
8498
+ return prop.key.name;
8499
+ }
8500
+ else if (t.isStringLiteral(prop.key)) {
8501
+ return prop.key.value;
8502
+ }
8503
+ }
8504
+ return 'unknown';
8505
+ });
8506
+ violations.push({
8507
+ rule: 'callbacks-passthrough-only',
8508
+ severity: 'critical',
8509
+ line: path.node.loc?.start.line || 0,
8510
+ column: path.node.loc?.start.column || 0,
8511
+ message: `Callbacks must be passed through unchanged. Found spreading with additional properties: ${addedProps.join(', ')}. Component events should be passed as direct props, not added to callbacks.`,
8512
+ suggestion: {
8513
+ text: `The callbacks prop should only contain OpenEntityRecord and RegisterMethod. Pass component events as separate props.`,
8514
+ example: `// ❌ WRONG - Modifying callbacks
8515
+ <ChildComponent
8516
+ callbacks={{ ...callbacks, onOpen: handleOpen }}
8517
+ />
8518
+
8519
+ // ✅ CORRECT - Pass callbacks unchanged, events as props
8520
+ <ChildComponent
8521
+ callbacks={callbacks}
8522
+ onOpen={handleOpen}
8523
+ />`
8524
+ }
8525
+ });
8526
+ }
8527
+ else if (expr.properties.length > 0) {
8528
+ // Creating new callbacks object
8529
+ violations.push({
8530
+ rule: 'callbacks-passthrough-only',
8531
+ severity: 'critical',
8532
+ line: path.node.loc?.start.line || 0,
8533
+ column: path.node.loc?.start.column || 0,
8534
+ message: `Callbacks must be passed through unchanged. Do not create new callback objects. Pass the callbacks prop directly.`,
8535
+ suggestion: {
8536
+ text: `Pass callbacks directly without modification.`,
8537
+ example: `// ❌ WRONG - Creating new callbacks object
8538
+ <ChildComponent
8539
+ callbacks={{ OpenEntityRecord: customHandler }}
8540
+ />
8541
+
8542
+ // ✅ CORRECT - Pass callbacks unchanged
8543
+ <ChildComponent
8544
+ callbacks={callbacks}
8545
+ />`
8546
+ }
8547
+ });
8548
+ }
8549
+ }
8550
+ // Check for conditional expressions
8551
+ else if (t.isConditionalExpression(expr) || t.isLogicalExpression(expr)) {
8552
+ violations.push({
8553
+ rule: 'callbacks-passthrough-only',
8554
+ severity: 'medium',
8555
+ line: path.node.loc?.start.line || 0,
8556
+ column: path.node.loc?.start.column || 0,
8557
+ message: `Callbacks should be passed through directly without conditional logic. Consider handling the condition at a higher level.`,
8558
+ suggestion: {
8559
+ text: `Pass callbacks directly or handle conditions in parent component.`,
8560
+ example: `// ⚠️ AVOID - Conditional callbacks
8561
+ <ChildComponent
8562
+ callbacks={someCondition ? callbacks : undefined}
8563
+ />
8564
+
8565
+ // ✅ BETTER - Pass callbacks directly
8566
+ <ChildComponent
8567
+ callbacks={callbacks}
8568
+ />`
8569
+ }
8570
+ });
8571
+ }
8572
+ // Check for function calls or other expressions
8573
+ else if (!t.isIdentifier(expr) && !t.isMemberExpression(expr)) {
8574
+ violations.push({
8575
+ rule: 'callbacks-passthrough-only',
8576
+ severity: 'critical',
8577
+ line: path.node.loc?.start.line || 0,
8578
+ column: path.node.loc?.start.column || 0,
8579
+ message: `Callbacks must be passed through unchanged. Found complex expression instead of direct passthrough.`,
8580
+ suggestion: {
8581
+ text: `Pass the callbacks prop directly without modification.`,
8582
+ example: `// ✅ CORRECT
8583
+ <ChildComponent callbacks={callbacks} />`
8584
+ }
8585
+ });
8586
+ }
8587
+ }
8588
+ }
8589
+ }
8590
+ },
8591
+ // Also check for Object.assign or spread operations on callbacks
8592
+ CallExpression(path) {
8593
+ // Check for Object.assign(callbacks, ...)
8594
+ if (t.isMemberExpression(path.node.callee) &&
8595
+ t.isIdentifier(path.node.callee.object) &&
8596
+ path.node.callee.object.name === 'Object' &&
8597
+ t.isIdentifier(path.node.callee.property) &&
8598
+ path.node.callee.property.name === 'assign') {
8599
+ const args = path.node.arguments;
8600
+ if (args.length > 0) {
8601
+ // Check if callbacks is being modified
8602
+ const hasCallbacks = args.some(arg => t.isIdentifier(arg) && arg.name === 'callbacks');
8603
+ if (hasCallbacks) {
8604
+ violations.push({
8605
+ rule: 'callbacks-passthrough-only',
8606
+ severity: 'critical',
8607
+ line: path.node.loc?.start.line || 0,
8608
+ column: path.node.loc?.start.column || 0,
8609
+ message: `Do not modify callbacks with Object.assign. Callbacks should be passed through unchanged.`,
8610
+ suggestion: {
8611
+ text: `Pass callbacks directly and use separate props for component events.`,
8612
+ example: `// ❌ WRONG
8613
+ const modifiedCallbacks = Object.assign({}, callbacks, { onOpen: handler });
8614
+
8615
+ // ✅ CORRECT - Keep callbacks separate from events
8616
+ <Component callbacks={callbacks} onOpen={handler} />`
8617
+ }
8618
+ });
8619
+ }
8620
+ }
8621
+ }
8622
+ },
8623
+ // Check for variable assignments that modify callbacks
8624
+ VariableDeclarator(path) {
8625
+ if (t.isObjectExpression(path.node.init)) {
8626
+ const hasCallbacksSpread = path.node.init.properties.some(prop => t.isSpreadElement(prop) &&
8627
+ t.isIdentifier(prop.argument) &&
8628
+ prop.argument.name === 'callbacks');
8629
+ if (hasCallbacksSpread) {
8630
+ const hasAdditionalProps = path.node.init.properties.some(prop => !t.isSpreadElement(prop));
8631
+ if (hasAdditionalProps) {
8632
+ violations.push({
8633
+ rule: 'callbacks-passthrough-only',
8634
+ severity: 'critical',
8635
+ line: path.node.loc?.start.line || 0,
8636
+ column: path.node.loc?.start.column || 0,
8637
+ message: `Do not create modified copies of callbacks. Pass callbacks unchanged and use separate props for events.`,
8638
+ suggestion: {
8639
+ text: `Keep callbacks immutable and pass component events as separate props.`,
8640
+ example: `// ❌ WRONG
8641
+ const extendedCallbacks = { ...callbacks, onCustomEvent: handler };
8642
+
8643
+ // ✅ CORRECT - Keep them separate
8644
+ // Pass to child component:
8645
+ <Component callbacks={callbacks} onCustomEvent={handler} />`
8646
+ }
8647
+ });
8648
+ }
8649
+ }
8650
+ }
8651
+ }
8652
+ });
8653
+ return violations;
8654
+ }
8099
8655
  }
8100
8656
  ];
8101
8657
  //# sourceMappingURL=component-linter.js.map