@memberjunction/react-test-harness 2.97.0 → 2.99.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.
@@ -196,6 +196,10 @@ class ComponentLinter {
196
196
  }
197
197
  static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode, options) {
198
198
  try {
199
+ // Require contextUser when libraries need to be checked
200
+ if (componentSpec?.libraries && componentSpec.libraries.length > 0 && !contextUser) {
201
+ throw new Error('contextUser is required when linting components with library dependencies. This is needed to load library-specific lint rules from the database.');
202
+ }
199
203
  // Parse with error recovery to get both AST and errors
200
204
  const parseResult = parser.parse(code, {
201
205
  sourceType: 'module',
@@ -259,7 +263,7 @@ class ComponentLinter {
259
263
  violations.push(...dataViolations);
260
264
  }
261
265
  // Apply library-specific lint rules if available
262
- if (componentSpec?.libraries && contextUser) {
266
+ if (componentSpec?.libraries) {
263
267
  const libraryViolations = await this.applyLibraryLintRules(ast, componentSpec, contextUser, debugMode);
264
268
  violations.push(...libraryViolations);
265
269
  }
@@ -1580,34 +1584,24 @@ await utilities.rq.RunQuery({
1580
1584
  break;
1581
1585
  case 'component-props-validation':
1582
1586
  violation.suggestion = {
1583
- 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.',
1584
1588
  example: `// ❌ WRONG - Component with undeclared props:
1585
1589
  function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
1586
- // 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
1587
1592
  }
1588
1593
 
1589
- // ✅ CORRECT Option 1 - Use only standard props and load data internally:
1594
+ // ✅ CORRECT - Use only standard props and props defined in the spec:
1590
1595
  function MyComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
1591
- // Load data internally using utilities
1596
+ // If you need data like customers/orders, load it internally using utilities
1592
1597
  const [customers, setCustomers] = useState([]);
1593
1598
  const [orders, setOrders] = useState([]);
1594
1599
  const [selectedId, setSelectedId] = useState(savedUserSettings?.selectedId);
1595
- }
1596
-
1597
- // ✅ CORRECT Option 2 - Define props in component spec:
1598
- // In spec.properties array:
1599
- // [
1600
- // { name: "customers", type: "array", required: false, description: "Customer list" },
1601
- // { name: "orders", type: "array", required: false, description: "Order list" },
1602
- // { name: "selectedId", type: "string", required: false, description: "Selected item ID" }
1603
- // ]
1604
- // Then the component can accept them:
1605
- function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
1606
- // These props are now allowed because they're defined in the spec
1607
1600
 
1608
1601
  useEffect(() => {
1609
1602
  const loadData = async () => {
1610
1603
  try {
1604
+ // Load customers data internally
1611
1605
  const result = await utilities.rv.RunView({
1612
1606
  EntityName: 'Customers',
1613
1607
  Fields: ['ID', 'Name', 'Status']
@@ -1622,8 +1616,12 @@ function MyComponent({ utilities, styles, components, customers, orders, selecte
1622
1616
  loadData();
1623
1617
  }, []);
1624
1618
 
1625
- return <div>{/* Use state, not props */}</div>;
1626
- }`
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.`
1627
1625
  };
1628
1626
  break;
1629
1627
  case 'runview-runquery-result-direct-usage':
@@ -1762,50 +1760,78 @@ const {
1762
1760
  const libraryViolations = [];
1763
1761
  // Get the cached and compiled rules for this library
1764
1762
  const compiledRules = cache.getLibraryRules(lib.name);
1763
+ if (debugMode) {
1764
+ console.log(`\n 📚 Library: ${lib.name}`);
1765
+ if (compiledRules) {
1766
+ console.log(` ┌─ Has lint rules: ✅`);
1767
+ if (compiledRules.validators) {
1768
+ console.log(` ├─ Validators: ${Object.keys(compiledRules.validators).length}`);
1769
+ }
1770
+ if (compiledRules.initialization) {
1771
+ console.log(` ├─ Initialization rules: ✅`);
1772
+ }
1773
+ if (compiledRules.lifecycle) {
1774
+ console.log(` ├─ Lifecycle rules: ✅`);
1775
+ }
1776
+ console.log(` └─ Starting checks...`);
1777
+ }
1778
+ else {
1779
+ console.log(` └─ No lint rules defined`);
1780
+ }
1781
+ }
1765
1782
  if (compiledRules) {
1766
1783
  const library = compiledRules.library;
1767
1784
  const libraryName = library.Name || lib.name;
1768
1785
  // Apply initialization rules
1769
1786
  if (compiledRules.initialization) {
1787
+ if (debugMode) {
1788
+ console.log(` ├─ 🔍 Checking ${libraryName} initialization patterns...`);
1789
+ }
1770
1790
  const initViolations = this.checkLibraryInitialization(ast, libraryName, compiledRules.initialization);
1771
1791
  // Debug logging for library violations
1772
1792
  if (debugMode && initViolations.length > 0) {
1773
- console.log(`\n🔍 ${libraryName} Initialization Violations Found:`);
1793
+ console.log(` │ ⚠️ Found ${initViolations.length} initialization issue${initViolations.length > 1 ? 's' : ''}`);
1774
1794
  initViolations.forEach(v => {
1775
1795
  const icon = v.severity === 'critical' ? '🔴' :
1776
1796
  v.severity === 'high' ? '🟠' :
1777
1797
  v.severity === 'medium' ? '🟡' : '🟢';
1778
- console.log(` ${icon} [${v.severity}] Line ${v.line}: ${v.message}`);
1798
+ console.log(` ${icon} Line ${v.line}: ${v.message}`);
1779
1799
  });
1780
1800
  }
1781
1801
  libraryViolations.push(...initViolations);
1782
1802
  }
1783
1803
  // Apply lifecycle rules
1784
1804
  if (compiledRules.lifecycle) {
1805
+ if (debugMode) {
1806
+ console.log(` ├─ 🔄 Checking ${libraryName} lifecycle management...`);
1807
+ }
1785
1808
  const lifecycleViolations = this.checkLibraryLifecycle(ast, libraryName, compiledRules.lifecycle);
1786
1809
  // Debug logging for library violations
1787
1810
  if (debugMode && lifecycleViolations.length > 0) {
1788
- console.log(`\n🔍 ${libraryName} Lifecycle Violations Found:`);
1811
+ console.log(` │ ⚠️ Found ${lifecycleViolations.length} lifecycle issue${lifecycleViolations.length > 1 ? 's' : ''}`);
1789
1812
  lifecycleViolations.forEach(v => {
1790
1813
  const icon = v.severity === 'critical' ? '🔴' :
1791
1814
  v.severity === 'high' ? '🟠' :
1792
1815
  v.severity === 'medium' ? '🟡' : '🟢';
1793
- console.log(` ${icon} [${v.severity}] Line ${v.line}: ${v.message}`);
1816
+ console.log(` ${icon} Line ${v.line}: ${v.message}`);
1794
1817
  });
1795
1818
  }
1796
1819
  libraryViolations.push(...lifecycleViolations);
1797
1820
  }
1798
1821
  // Apply options validation
1799
1822
  if (compiledRules.options) {
1823
+ if (debugMode) {
1824
+ console.log(` ├─ ⚙️ Checking ${libraryName} configuration options...`);
1825
+ }
1800
1826
  const optionsViolations = this.checkLibraryOptions(ast, libraryName, compiledRules.options);
1801
1827
  // Debug logging for library violations
1802
1828
  if (debugMode && optionsViolations.length > 0) {
1803
- console.log(`\n🔍 ${libraryName} Options Violations Found:`);
1829
+ console.log(` │ ⚠️ Found ${optionsViolations.length} configuration issue${optionsViolations.length > 1 ? 's' : ''}`);
1804
1830
  optionsViolations.forEach(v => {
1805
1831
  const icon = v.severity === 'critical' ? '🔴' :
1806
1832
  v.severity === 'high' ? '🟠' :
1807
1833
  v.severity === 'medium' ? '🟡' : '🟢';
1808
- console.log(` ${icon} [${v.severity}] Line ${v.line}: ${v.message}`);
1834
+ console.log(` ${icon} Line ${v.line}: ${v.message}`);
1809
1835
  });
1810
1836
  }
1811
1837
  libraryViolations.push(...optionsViolations);
@@ -2105,6 +2131,13 @@ const {
2105
2131
  for (const [validatorName, validator] of Object.entries(validators)) {
2106
2132
  if (validator && validator.validateFn) {
2107
2133
  const beforeCount = context.violations.length;
2134
+ // Log that we're running this specific validator
2135
+ if (debugMode) {
2136
+ console.log(` ├─ 🔬 Running ${libraryName} validator: ${validatorName}`);
2137
+ if (validator.description) {
2138
+ console.log(` │ ℹ️ ${validator.description}`);
2139
+ }
2140
+ }
2108
2141
  // Traverse AST and apply validator
2109
2142
  (0, traverse_1.default)(ast, {
2110
2143
  enter(path) {
@@ -2115,27 +2148,31 @@ const {
2115
2148
  catch (error) {
2116
2149
  // Validator execution error - log but don't crash
2117
2150
  console.warn(`Validator ${validatorName} failed:`, error);
2151
+ if (debugMode) {
2152
+ console.error('Full error:', error);
2153
+ }
2118
2154
  }
2119
2155
  }
2120
2156
  });
2121
2157
  // Debug logging for this specific validator
2122
2158
  const newViolations = context.violations.length - beforeCount;
2123
2159
  if (debugMode && newViolations > 0) {
2124
- console.log(`\n📋 ${libraryName} - ${validatorName}:`);
2125
- console.log(` 📊 ${validator.description || 'No description'}`);
2126
- console.log(` ⚠️ Found ${newViolations} violation${newViolations > 1 ? 's' : ''}`);
2160
+ console.log(` │ ✓ Found ${newViolations} violation${newViolations > 1 ? 's' : ''}`);
2127
2161
  // Show the violations from this validator
2128
2162
  const validatorViolations = context.violations.slice(beforeCount);
2129
2163
  validatorViolations.forEach((v) => {
2130
2164
  const icon = v.type === 'error' || v.severity === 'critical' ? '🔴' :
2131
2165
  v.type === 'warning' || v.severity === 'high' ? '🟠' :
2132
2166
  v.severity === 'medium' ? '🟡' : '🟢';
2133
- console.log(` ${icon} Line ${v.line || 'unknown'}: ${v.message}`);
2167
+ console.log(`${icon} Line ${v.line || 'unknown'}: ${v.message}`);
2134
2168
  if (v.suggestion) {
2135
- console.log(` 💡 ${v.suggestion}`);
2169
+ console.log(`💡 ${v.suggestion}`);
2136
2170
  }
2137
2171
  });
2138
2172
  }
2173
+ else if (debugMode) {
2174
+ console.log(` │ ✓ No violations found`);
2175
+ }
2139
2176
  }
2140
2177
  }
2141
2178
  // Convert context violations to standard format
@@ -2635,7 +2672,7 @@ ComponentLinter.universalComponentRules = [
2635
2672
  severity: 'critical',
2636
2673
  line: path.node.loc?.start.line || 0,
2637
2674
  column: path.node.loc?.start.column || 0,
2638
- 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.`,
2639
2676
  code: path.toString().substring(0, 100)
2640
2677
  });
2641
2678
  }
@@ -2873,6 +2910,66 @@ ComponentLinter.universalComponentRules = [
2873
2910
  return violations;
2874
2911
  }
2875
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
+ },
2876
2973
  {
2877
2974
  name: 'library-variable-names',
2878
2975
  appliesTo: 'all',
@@ -2905,7 +3002,7 @@ ComponentLinter.universalComponentRules = [
2905
3002
  severity: 'critical',
2906
3003
  line: path.node.loc?.start.line || 0,
2907
3004
  column: path.node.loc?.start.column || 0,
2908
- 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}, [...]);"`
2909
3006
  });
2910
3007
  }
2911
3008
  }
@@ -4919,6 +5016,92 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4919
5016
  return violations;
4920
5017
  }
4921
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
+ },
4922
5105
  {
4923
5106
  name: 'component-props-validation',
4924
5107
  appliesTo: 'all',
@@ -4927,8 +5110,10 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4927
5110
  const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
4928
5111
  // React special props that are automatically provided by React
4929
5112
  const reactSpecialProps = new Set(['children']);
4930
- // 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
4931
5114
  const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
5115
+ // Track required props separately for validation
5116
+ const requiredProps = new Set();
4932
5117
  // Add props from componentSpec.properties if they exist
4933
5118
  // These are the architect-defined props that this component is allowed to accept
4934
5119
  const specDefinedProps = [];
@@ -4937,6 +5122,21 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4937
5122
  if (prop.name) {
4938
5123
  allowedProps.add(prop.name);
4939
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
4940
5140
  }
4941
5141
  }
4942
5142
  }
@@ -4956,23 +5156,36 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
4956
5156
  }
4957
5157
  }
4958
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
+ }
4959
5171
  // Only report if there are non-allowed props
4960
5172
  if (invalidProps.length > 0) {
4961
5173
  let message;
4962
- if (specDefinedProps.length > 0) {
5174
+ if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
4963
5175
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
4964
5176
  `This component can only accept: ` +
4965
5177
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
4966
- `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
4967
- `(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
4968
- `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.`;
4969
5182
  }
4970
5183
  else {
4971
5184
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
4972
5185
  `This component can only accept: ` +
4973
5186
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
4974
5187
  `(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
4975
- `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.`;
4976
5189
  }
4977
5190
  violations.push({
4978
5191
  rule: 'component-props-validation',
@@ -5003,28 +5216,41 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5003
5216
  }
5004
5217
  }
5005
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
+ }
5006
5231
  if (invalidProps.length > 0) {
5007
5232
  let message;
5008
- if (specDefinedProps.length > 0) {
5233
+ if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
5009
5234
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5010
5235
  `This component can only accept: ` +
5011
5236
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5012
- `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
5013
- `(3) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5014
- `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.`;
5015
5241
  }
5016
5242
  else {
5017
5243
  message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
5018
5244
  `This component can only accept: ` +
5019
5245
  `(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
5020
5246
  `(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
5021
- `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.`;
5022
5248
  }
5023
5249
  violations.push({
5024
5250
  rule: 'component-props-validation',
5025
5251
  severity: 'critical',
5026
- line: path.node.loc?.start.line || 0,
5027
- column: path.node.loc?.start.column || 0,
5252
+ line: init.loc?.start.line || 0,
5253
+ column: init.loc?.start.column || 0,
5028
5254
  message
5029
5255
  });
5030
5256
  }
@@ -5625,7 +5851,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5625
5851
  severity: 'critical',
5626
5852
  line: openingElement.loc?.start.line || 0,
5627
5853
  column: openingElement.loc?.start.column || 0,
5628
- 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.`,
5629
5855
  code: `<${tagName} ... />`
5630
5856
  });
5631
5857
  } else {
@@ -5635,7 +5861,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5635
5861
  severity: 'critical',
5636
5862
  line: openingElement.loc?.start.line || 0,
5637
5863
  column: openingElement.loc?.start.column || 0,
5638
- 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}']);`,
5639
5865
  code: `<${tagName} ... />`
5640
5866
  });
5641
5867
  }
@@ -5668,7 +5894,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5668
5894
  severity: 'high',
5669
5895
  line: openingElement.loc?.start.line || 0,
5670
5896
  column: openingElement.loc?.start.column || 0,
5671
- 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.`,
5672
5898
  code: `<${tagName} ... />`
5673
5899
  });
5674
5900
  }
@@ -5696,42 +5922,13 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5696
5922
  appliesTo: 'all',
5697
5923
  test: (ast, componentName, componentSpec) => {
5698
5924
  const violations = [];
5699
- // Extract declared queries and entities from dataRequirements
5700
- const declaredQueries = new Set();
5701
- const declaredEntities = new Set();
5702
- if (componentSpec?.dataRequirements) {
5703
- // Handle queries in different possible locations
5704
- if (Array.isArray(componentSpec.dataRequirements)) {
5705
- // If it's an array directly
5706
- componentSpec.dataRequirements.forEach((req) => {
5707
- if (req.type === 'query' && req.name) {
5708
- declaredQueries.add(req.name.toLowerCase());
5709
- }
5710
- if (req.type === 'entity' && req.name) {
5711
- declaredEntities.add(req.name.toLowerCase());
5712
- }
5713
- });
5714
- }
5715
- else if (typeof componentSpec.dataRequirements === 'object') {
5716
- // If it's an object with queries/entities properties
5717
- if (componentSpec.dataRequirements.queries) {
5718
- componentSpec.dataRequirements.queries.forEach((q) => {
5719
- if (q.name)
5720
- declaredQueries.add(q.name.toLowerCase());
5721
- });
5722
- }
5723
- if (componentSpec.dataRequirements.entities) {
5724
- componentSpec.dataRequirements.entities.forEach((e) => {
5725
- if (e.name)
5726
- declaredEntities.add(e.name.toLowerCase());
5727
- });
5728
- }
5729
- }
5730
- }
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
5731
5928
  (0, traverse_1.default)(ast, {
5732
5929
  CallExpression(path) {
5733
5930
  const callee = path.node.callee;
5734
- // Check for RunQuery calls
5931
+ // Check for RunQuery calls - focus on SQL injection detection
5735
5932
  if (t.isMemberExpression(callee) &&
5736
5933
  t.isIdentifier(callee.property) &&
5737
5934
  callee.property.name === 'RunQuery') {
@@ -5761,20 +5958,9 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5761
5958
  code: value.value.substring(0, 100)
5762
5959
  });
5763
5960
  }
5764
- else if (declaredQueries.size > 0 && !declaredQueries.has(queryName.toLowerCase())) {
5765
- // Only validate if we have declared queries
5766
- violations.push({
5767
- rule: 'runquery-runview-validation',
5768
- severity: 'high',
5769
- line: value.loc?.start.line || 0,
5770
- column: value.loc?.start.column || 0,
5771
- message: `Query "${queryName}" is not declared in dataRequirements.queries. Available queries: ${Array.from(declaredQueries).join(', ')}`,
5772
- code: path.toString().substring(0, 100)
5773
- });
5774
- }
5775
5961
  }
5776
5962
  else if (t.isIdentifier(value) || t.isTemplateLiteral(value)) {
5777
- // Dynamic query name - check if it might be SQL
5963
+ // Dynamic query name - warn that it shouldn't be SQL
5778
5964
  violations.push({
5779
5965
  rule: 'runquery-runview-validation',
5780
5966
  severity: 'medium',
@@ -5787,43 +5973,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
5787
5973
  }
5788
5974
  }
5789
5975
  }
5790
- // Check for RunView calls
5791
- if (t.isMemberExpression(callee) &&
5792
- t.isIdentifier(callee.property) &&
5793
- (callee.property.name === 'RunView' || callee.property.name === 'RunViews')) {
5794
- const args = path.node.arguments;
5795
- // Handle both single object and array of objects
5796
- const checkEntityName = (objExpr) => {
5797
- const entityNameProp = objExpr.properties.find(p => t.isObjectProperty(p) &&
5798
- t.isIdentifier(p.key) &&
5799
- p.key.name === 'EntityName');
5800
- if (entityNameProp && t.isObjectProperty(entityNameProp) && t.isStringLiteral(entityNameProp.value)) {
5801
- const entityName = entityNameProp.value.value;
5802
- if (declaredEntities.size > 0 && !declaredEntities.has(entityName.toLowerCase())) {
5803
- violations.push({
5804
- rule: 'runquery-runview-validation',
5805
- severity: 'high',
5806
- line: entityNameProp.value.loc?.start.line || 0,
5807
- column: entityNameProp.value.loc?.start.column || 0,
5808
- message: `Entity "${entityName}" is not declared in dataRequirements.entities. Available entities: ${Array.from(declaredEntities).join(', ')}`,
5809
- code: path.toString().substring(0, 100)
5810
- });
5811
- }
5812
- }
5813
- };
5814
- if (args.length > 0) {
5815
- if (t.isObjectExpression(args[0])) {
5816
- checkEntityName(args[0]);
5817
- }
5818
- else if (t.isArrayExpression(args[0])) {
5819
- args[0].elements.forEach(elem => {
5820
- if (t.isObjectExpression(elem)) {
5821
- checkEntityName(elem);
5822
- }
5823
- });
5824
- }
5825
- }
5826
- }
5976
+ // RunView validation removed - handled by data-requirements-validation
5827
5977
  }
5828
5978
  });
5829
5979
  return violations;
@@ -8053,6 +8203,263 @@ const [state, setState] = useState(initialValue);`
8053
8203
  });
8054
8204
  return violations;
8055
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
+ }
8056
8463
  }
8057
8464
  ];
8058
8465
  //# sourceMappingURL=component-linter.js.map