@memberjunction/react-test-harness 2.91.0 → 2.93.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.
@@ -30,6 +30,7 @@ exports.ComponentLinter = void 0;
30
30
  const parser = __importStar(require("@babel/parser"));
31
31
  const traverse_1 = __importDefault(require("@babel/traverse"));
32
32
  const t = __importStar(require("@babel/types"));
33
+ const library_lint_cache_1 = require("./library-lint-cache");
33
34
  // Standard HTML elements (lowercase)
34
35
  const HTML_ELEMENTS = new Set([
35
36
  // Main root
@@ -97,6 +98,22 @@ const runViewResultProps = [
97
98
  'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
98
99
  ];
99
100
  class ComponentLinter {
101
+ // Helper method to check if a statement contains a return
102
+ static containsReturn(node) {
103
+ let hasReturn = false;
104
+ // Create a mini AST to traverse
105
+ const file = t.file(t.program([t.expressionStatement(node)]));
106
+ (0, traverse_1.default)(file, {
107
+ ReturnStatement(path) {
108
+ // Don't count returns in nested functions
109
+ const parent = path.getFunctionParent();
110
+ if (!parent || parent.node === node) {
111
+ hasReturn = true;
112
+ }
113
+ }
114
+ });
115
+ return hasReturn;
116
+ }
100
117
  // Helper method to check if a variable comes from RunQuery or RunView
101
118
  static isVariableFromRunQueryOrView(path, varName, methodName) {
102
119
  let isFromMethod = false;
@@ -135,7 +152,7 @@ class ComponentLinter {
135
152
  }
136
153
  return isFromMethod;
137
154
  }
138
- static async lintComponent(code, componentName, componentSpec, isRootComponent) {
155
+ static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode, options) {
139
156
  try {
140
157
  const ast = parser.parse(code, {
141
158
  sourceType: 'module',
@@ -156,7 +173,7 @@ class ComponentLinter {
156
173
  const violations = [];
157
174
  // Run each rule
158
175
  for (const rule of rules) {
159
- const ruleViolations = rule.test(ast, componentName, componentSpec);
176
+ const ruleViolations = rule.test(ast, componentName, componentSpec, options);
160
177
  violations.push(...ruleViolations);
161
178
  }
162
179
  // Add data requirements validation if componentSpec is provided
@@ -164,6 +181,11 @@ class ComponentLinter {
164
181
  const dataViolations = this.validateDataRequirements(ast, componentSpec);
165
182
  violations.push(...dataViolations);
166
183
  }
184
+ // Apply library-specific lint rules if available
185
+ if (componentSpec?.libraries && contextUser) {
186
+ const libraryViolations = await this.applyLibraryLintRules(ast, componentSpec, contextUser, debugMode);
187
+ violations.push(...libraryViolations);
188
+ }
167
189
  // Deduplicate violations - keep only unique rule+message combinations
168
190
  const uniqueViolations = this.deduplicateViolations(violations);
169
191
  // Count violations by severity
@@ -171,6 +193,37 @@ class ComponentLinter {
171
193
  const highCount = uniqueViolations.filter(v => v.severity === 'high').length;
172
194
  const mediumCount = uniqueViolations.filter(v => v.severity === 'medium').length;
173
195
  const lowCount = uniqueViolations.filter(v => v.severity === 'low').length;
196
+ // Debug mode summary
197
+ if (debugMode && uniqueViolations.length > 0) {
198
+ console.log('\n' + '='.repeat(60));
199
+ console.log('šŸ“Š LINT SUMMARY:');
200
+ console.log('='.repeat(60));
201
+ if (criticalCount > 0)
202
+ console.log(` šŸ”“ Critical: ${criticalCount}`);
203
+ if (highCount > 0)
204
+ console.log(` 🟠 High: ${highCount}`);
205
+ if (mediumCount > 0)
206
+ console.log(` 🟔 Medium: ${mediumCount}`);
207
+ if (lowCount > 0)
208
+ console.log(` 🟢 Low: ${lowCount}`);
209
+ console.log('='.repeat(60));
210
+ // Group violations by library
211
+ const libraryViolations = uniqueViolations.filter(v => v.rule.includes('-validator'));
212
+ if (libraryViolations.length > 0) {
213
+ console.log('\nšŸ“š Library-Specific Issues:');
214
+ const byLibrary = new Map();
215
+ libraryViolations.forEach(v => {
216
+ const lib = v.rule.replace('-validator', '');
217
+ if (!byLibrary.has(lib))
218
+ byLibrary.set(lib, []);
219
+ byLibrary.get(lib).push(v);
220
+ });
221
+ byLibrary.forEach((violations, library) => {
222
+ console.log(` • ${library}: ${violations.length} issue${violations.length > 1 ? 's' : ''}`);
223
+ });
224
+ }
225
+ console.log('');
226
+ }
174
227
  // Generate fix suggestions
175
228
  const suggestions = this.generateFixSuggestions(uniqueViolations);
176
229
  return {
@@ -1450,10 +1503,10 @@ await utilities.rq.RunQuery({
1450
1503
  // - CategoryName, CategoryID, Parameters (optional)`
1451
1504
  });
1452
1505
  break;
1453
- case 'root-component-props-restriction':
1506
+ case 'component-props-validation':
1454
1507
  suggestions.push({
1455
1508
  violation: violation.rule,
1456
- suggestion: 'Root components can only accept standard props. Load data internally.',
1509
+ suggestion: 'Components can only accept standard props or props defined in spec. Load data internally.',
1457
1510
  example: `// āŒ WRONG - Root component with additional props:
1458
1511
  function RootComponent({ utilities, styles, components, customers, orders, selectedId }) {
1459
1512
  // Additional props will break hosting environment
@@ -1542,6 +1595,410 @@ setData(queryResult.Results || []); // NOT queryResult directly!
1542
1595
  }
1543
1596
  return suggestions;
1544
1597
  }
1598
+ /**
1599
+ * Apply library-specific lint rules based on ComponentLibrary LintRules field
1600
+ */
1601
+ static async applyLibraryLintRules(ast, componentSpec, contextUser, debugMode) {
1602
+ const violations = [];
1603
+ try {
1604
+ // Use the cached and compiled library rules
1605
+ const cache = library_lint_cache_1.LibraryLintCache.getInstance();
1606
+ await cache.loadLibraryRules(contextUser);
1607
+ // Check each library that this component uses
1608
+ if (componentSpec.libraries) {
1609
+ // Run library checks in parallel for performance
1610
+ const libraryPromises = componentSpec.libraries.map(async (lib) => {
1611
+ const libraryViolations = [];
1612
+ // Get the cached and compiled rules for this library
1613
+ const compiledRules = cache.getLibraryRules(lib.name);
1614
+ if (compiledRules) {
1615
+ const library = compiledRules.library;
1616
+ const libraryName = library.Name || lib.name;
1617
+ // Apply initialization rules
1618
+ if (compiledRules.initialization) {
1619
+ const initViolations = this.checkLibraryInitialization(ast, libraryName, compiledRules.initialization);
1620
+ // Debug logging for library violations
1621
+ if (debugMode && initViolations.length > 0) {
1622
+ console.log(`\nšŸ” ${libraryName} Initialization Violations Found:`);
1623
+ initViolations.forEach(v => {
1624
+ const icon = v.severity === 'critical' ? 'šŸ”“' :
1625
+ v.severity === 'high' ? '🟠' :
1626
+ v.severity === 'medium' ? '🟔' : '🟢';
1627
+ console.log(` ${icon} [${v.severity}] Line ${v.line}: ${v.message}`);
1628
+ });
1629
+ }
1630
+ libraryViolations.push(...initViolations);
1631
+ }
1632
+ // Apply lifecycle rules
1633
+ if (compiledRules.lifecycle) {
1634
+ const lifecycleViolations = this.checkLibraryLifecycle(ast, libraryName, compiledRules.lifecycle);
1635
+ // Debug logging for library violations
1636
+ if (debugMode && lifecycleViolations.length > 0) {
1637
+ console.log(`\nšŸ” ${libraryName} Lifecycle Violations Found:`);
1638
+ lifecycleViolations.forEach(v => {
1639
+ const icon = v.severity === 'critical' ? 'šŸ”“' :
1640
+ v.severity === 'high' ? '🟠' :
1641
+ v.severity === 'medium' ? '🟔' : '🟢';
1642
+ console.log(` ${icon} [${v.severity}] Line ${v.line}: ${v.message}`);
1643
+ });
1644
+ }
1645
+ libraryViolations.push(...lifecycleViolations);
1646
+ }
1647
+ // Apply options validation
1648
+ if (compiledRules.options) {
1649
+ const optionsViolations = this.checkLibraryOptions(ast, libraryName, compiledRules.options);
1650
+ // Debug logging for library violations
1651
+ if (debugMode && optionsViolations.length > 0) {
1652
+ console.log(`\nšŸ” ${libraryName} Options Violations Found:`);
1653
+ optionsViolations.forEach(v => {
1654
+ const icon = v.severity === 'critical' ? 'šŸ”“' :
1655
+ v.severity === 'high' ? '🟠' :
1656
+ v.severity === 'medium' ? '🟔' : '🟢';
1657
+ console.log(` ${icon} [${v.severity}] Line ${v.line}: ${v.message}`);
1658
+ });
1659
+ }
1660
+ libraryViolations.push(...optionsViolations);
1661
+ }
1662
+ // Apply compiled validators (already compiled in cache)
1663
+ if (compiledRules.validators) {
1664
+ const validatorViolations = this.executeCompiledValidators(ast, libraryName, library.GlobalVariable || '', compiledRules.validators, debugMode);
1665
+ libraryViolations.push(...validatorViolations);
1666
+ }
1667
+ }
1668
+ return libraryViolations;
1669
+ });
1670
+ // Wait for all library checks to complete
1671
+ const allLibraryViolations = await Promise.all(libraryPromises);
1672
+ // Flatten the results
1673
+ allLibraryViolations.forEach(libViolations => {
1674
+ violations.push(...libViolations);
1675
+ });
1676
+ }
1677
+ }
1678
+ catch (error) {
1679
+ console.warn('Failed to apply library lint rules:', error);
1680
+ }
1681
+ return violations;
1682
+ }
1683
+ /**
1684
+ * Check library initialization patterns (constructor, element type, etc.)
1685
+ */
1686
+ static checkLibraryInitialization(ast, libraryName, rules) {
1687
+ const violations = [];
1688
+ (0, traverse_1.default)(ast, {
1689
+ // Check for new ConstructorName() patterns
1690
+ NewExpression(path) {
1691
+ if (t.isIdentifier(path.node.callee) &&
1692
+ path.node.callee.name === rules.constructorName) {
1693
+ // Check if it requires 'new' keyword
1694
+ if (rules.requiresNew === false) {
1695
+ violations.push({
1696
+ rule: 'library-initialization',
1697
+ severity: 'critical',
1698
+ line: path.node.loc?.start.line || 0,
1699
+ column: path.node.loc?.start.column || 0,
1700
+ message: `${libraryName}: ${rules.constructorName} should not use 'new' keyword`,
1701
+ code: `${rules.constructorName}(...) // without new`
1702
+ });
1703
+ }
1704
+ // Check element type if first argument is a ref
1705
+ if (rules.elementType && path.node.arguments[0]) {
1706
+ const firstArg = path.node.arguments[0];
1707
+ // Check if it's chartRef.current or similar
1708
+ if (t.isMemberExpression(firstArg) &&
1709
+ t.isIdentifier(firstArg.property) &&
1710
+ firstArg.property.name === 'current') {
1711
+ // Try to find what element the ref is attached to
1712
+ const refName = t.isIdentifier(firstArg.object) ? firstArg.object.name : null;
1713
+ if (refName) {
1714
+ ComponentLinter.checkRefElementType(ast, refName, rules.elementType, libraryName, violations);
1715
+ }
1716
+ }
1717
+ }
1718
+ }
1719
+ },
1720
+ // Check for function calls without new (if requiresNew is true)
1721
+ CallExpression(path) {
1722
+ if (t.isIdentifier(path.node.callee) &&
1723
+ path.node.callee.name === rules.constructorName &&
1724
+ rules.requiresNew === true) {
1725
+ violations.push({
1726
+ rule: 'library-initialization',
1727
+ severity: 'critical',
1728
+ line: path.node.loc?.start.line || 0,
1729
+ column: path.node.loc?.start.column || 0,
1730
+ message: `${libraryName}: ${rules.constructorName} requires 'new' keyword`,
1731
+ code: `new ${rules.constructorName}(...)`
1732
+ });
1733
+ }
1734
+ }
1735
+ });
1736
+ return violations;
1737
+ }
1738
+ /**
1739
+ * Check if a ref is attached to the correct element type
1740
+ */
1741
+ static checkRefElementType(ast, refName, expectedType, libraryName, violations) {
1742
+ (0, traverse_1.default)(ast, {
1743
+ JSXElement(path) {
1744
+ const openingElement = path.node.openingElement;
1745
+ // Check if this element has a ref attribute
1746
+ const refAttr = openingElement.attributes.find(attr => t.isJSXAttribute(attr) &&
1747
+ t.isJSXIdentifier(attr.name) &&
1748
+ attr.name.name === 'ref');
1749
+ if (refAttr && t.isJSXAttribute(refAttr)) {
1750
+ // Check if the ref value matches our refName
1751
+ const refValue = refAttr.value;
1752
+ if (t.isJSXExpressionContainer(refValue) &&
1753
+ t.isIdentifier(refValue.expression) &&
1754
+ refValue.expression.name === refName) {
1755
+ // Check element type
1756
+ const elementName = t.isJSXIdentifier(openingElement.name)
1757
+ ? openingElement.name.name
1758
+ : '';
1759
+ if (elementName.toLowerCase() !== expectedType.toLowerCase()) {
1760
+ violations.push({
1761
+ rule: 'library-element-type',
1762
+ severity: 'critical',
1763
+ line: openingElement.loc?.start.line || 0,
1764
+ column: openingElement.loc?.start.column || 0,
1765
+ message: `${libraryName} requires a <${expectedType}> element, not <${elementName}>`,
1766
+ code: `<${expectedType} ref={${refName}}>`
1767
+ });
1768
+ }
1769
+ }
1770
+ }
1771
+ }
1772
+ });
1773
+ }
1774
+ /**
1775
+ * Check library lifecycle methods (render, destroy, etc.)
1776
+ */
1777
+ static checkLibraryLifecycle(ast, libraryName, rules) {
1778
+ const violations = [];
1779
+ // Track which methods are called
1780
+ const calledMethods = new Set();
1781
+ const instanceVariables = new Set();
1782
+ (0, traverse_1.default)(ast, {
1783
+ // Track instance variables
1784
+ VariableDeclarator(path) {
1785
+ if (t.isNewExpression(path.node.init) &&
1786
+ t.isIdentifier(path.node.init.callee)) {
1787
+ if (t.isIdentifier(path.node.id)) {
1788
+ instanceVariables.add(path.node.id.name);
1789
+ }
1790
+ }
1791
+ },
1792
+ // Track method calls
1793
+ CallExpression(path) {
1794
+ if (t.isMemberExpression(path.node.callee)) {
1795
+ const callee = path.node.callee;
1796
+ if (t.isIdentifier(callee.property)) {
1797
+ const methodName = callee.property.name;
1798
+ const objectName = t.isIdentifier(callee.object)
1799
+ ? callee.object.name
1800
+ : null;
1801
+ if (objectName && instanceVariables.has(objectName)) {
1802
+ calledMethods.add(methodName);
1803
+ }
1804
+ }
1805
+ }
1806
+ }
1807
+ });
1808
+ // Check required methods
1809
+ if (rules.requiredMethods) {
1810
+ for (const method of rules.requiredMethods) {
1811
+ if (!calledMethods.has(method)) {
1812
+ violations.push({
1813
+ rule: 'library-lifecycle',
1814
+ severity: 'high',
1815
+ line: 0,
1816
+ column: 0,
1817
+ message: `${libraryName}: Missing required method call '${method}()' after initialization`,
1818
+ code: `instance.${method}()`
1819
+ });
1820
+ }
1821
+ }
1822
+ }
1823
+ // Check cleanup in useEffect
1824
+ if (rules.cleanupMethods && rules.cleanupMethods.length > 0) {
1825
+ let hasCleanup = false;
1826
+ (0, traverse_1.default)(ast, {
1827
+ CallExpression(path) {
1828
+ if (t.isIdentifier(path.node.callee) &&
1829
+ path.node.callee.name === 'useEffect') {
1830
+ const firstArg = path.node.arguments[0];
1831
+ if (t.isArrowFunctionExpression(firstArg) || t.isFunctionExpression(firstArg)) {
1832
+ // Check if it returns a cleanup function
1833
+ (0, traverse_1.default)(firstArg, {
1834
+ ReturnStatement(returnPath) {
1835
+ if (t.isArrowFunctionExpression(returnPath.node.argument) ||
1836
+ t.isFunctionExpression(returnPath.node.argument)) {
1837
+ // Check if cleanup function calls destroy
1838
+ (0, traverse_1.default)(returnPath.node.argument, {
1839
+ CallExpression(cleanupPath) {
1840
+ if (t.isMemberExpression(cleanupPath.node.callee)) {
1841
+ const callee = cleanupPath.node.callee;
1842
+ if (t.isIdentifier(callee.property) &&
1843
+ rules.cleanupMethods.includes(callee.property.name)) {
1844
+ hasCleanup = true;
1845
+ }
1846
+ }
1847
+ }
1848
+ }, returnPath.scope, returnPath.state, returnPath);
1849
+ }
1850
+ }
1851
+ }, path.scope, path.state, path);
1852
+ }
1853
+ }
1854
+ }
1855
+ });
1856
+ if (!hasCleanup) {
1857
+ violations.push({
1858
+ rule: 'library-cleanup',
1859
+ severity: 'medium',
1860
+ line: 0,
1861
+ column: 0,
1862
+ message: `${libraryName}: Missing cleanup in useEffect. Call ${rules.cleanupMethods.join(' or ')} in cleanup function`,
1863
+ code: `useEffect(() => {\n // ... initialization\n return () => {\n instance.${rules.cleanupMethods[0]}();\n };\n}, []);`
1864
+ });
1865
+ }
1866
+ }
1867
+ return violations;
1868
+ }
1869
+ /**
1870
+ * Check library options and configuration
1871
+ */
1872
+ static checkLibraryOptions(ast, libraryName, rules) {
1873
+ const violations = [];
1874
+ (0, traverse_1.default)(ast, {
1875
+ ObjectExpression(path) {
1876
+ // Check if this might be a config object for the library
1877
+ const properties = path.node.properties
1878
+ .filter((p) => t.isObjectProperty(p));
1879
+ const propNames = properties
1880
+ .filter(p => t.isIdentifier(p.key))
1881
+ .map(p => p.key.name);
1882
+ // Check for required properties
1883
+ if (rules.requiredProperties) {
1884
+ const hasChartType = propNames.some(name => rules.requiredProperties.includes(name));
1885
+ if (hasChartType) {
1886
+ // This looks like a config object, check all required props
1887
+ for (const required of rules.requiredProperties) {
1888
+ if (!propNames.includes(required)) {
1889
+ violations.push({
1890
+ rule: 'library-options',
1891
+ severity: 'high',
1892
+ line: path.node.loc?.start.line || 0,
1893
+ column: path.node.loc?.start.column || 0,
1894
+ message: `${libraryName}: Missing required option '${required}'`,
1895
+ code: `${required}: /* value */`
1896
+ });
1897
+ }
1898
+ }
1899
+ }
1900
+ }
1901
+ // Check property types
1902
+ if (rules.propertyTypes) {
1903
+ for (const prop of properties) {
1904
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
1905
+ const propName = prop.key.name;
1906
+ const expectedType = rules.propertyTypes[propName];
1907
+ if (expectedType) {
1908
+ // Check if the value matches expected type
1909
+ if (expectedType.includes('array') && !t.isArrayExpression(prop.value)) {
1910
+ violations.push({
1911
+ rule: 'library-options',
1912
+ severity: 'medium',
1913
+ line: prop.loc?.start.line || 0,
1914
+ column: prop.loc?.start.column || 0,
1915
+ message: `${libraryName}: Option '${propName}' should be an array`,
1916
+ code: `${propName}: []`
1917
+ });
1918
+ }
1919
+ }
1920
+ }
1921
+ }
1922
+ }
1923
+ }
1924
+ });
1925
+ return violations;
1926
+ }
1927
+ /**
1928
+ * Execute pre-compiled validators from cache
1929
+ */
1930
+ static executeCompiledValidators(ast, libraryName, globalVariable, validators, debugMode) {
1931
+ const violations = [];
1932
+ // Create context object for validators
1933
+ const context = {
1934
+ libraryName,
1935
+ globalVariable,
1936
+ instanceVariables: new Set(),
1937
+ violations: [] // Validators push violations here
1938
+ };
1939
+ // First pass: identify library instance variables
1940
+ (0, traverse_1.default)(ast, {
1941
+ VariableDeclarator(path) {
1942
+ if (t.isNewExpression(path.node.init) &&
1943
+ t.isIdentifier(path.node.init.callee)) {
1944
+ // Check if it's a library constructor
1945
+ if (path.node.init.callee.name === globalVariable) {
1946
+ if (t.isIdentifier(path.node.id)) {
1947
+ context.instanceVariables.add(path.node.id.name);
1948
+ }
1949
+ }
1950
+ }
1951
+ }
1952
+ });
1953
+ // Execute each compiled validator
1954
+ for (const [validatorName, validator] of Object.entries(validators)) {
1955
+ if (validator && validator.validateFn) {
1956
+ const beforeCount = context.violations.length;
1957
+ // Traverse AST and apply validator
1958
+ (0, traverse_1.default)(ast, {
1959
+ enter(path) {
1960
+ try {
1961
+ // Validators don't return violations, they push to context.violations
1962
+ validator.validateFn(ast, path, t, context);
1963
+ }
1964
+ catch (error) {
1965
+ // Validator execution error - log but don't crash
1966
+ console.warn(`Validator ${validatorName} failed:`, error);
1967
+ }
1968
+ }
1969
+ });
1970
+ // Debug logging for this specific validator
1971
+ const newViolations = context.violations.length - beforeCount;
1972
+ if (debugMode && newViolations > 0) {
1973
+ console.log(`\nšŸ“‹ ${libraryName} - ${validatorName}:`);
1974
+ console.log(` šŸ“Š ${validator.description || 'No description'}`);
1975
+ console.log(` āš ļø Found ${newViolations} violation${newViolations > 1 ? 's' : ''}`);
1976
+ // Show the violations from this validator
1977
+ const validatorViolations = context.violations.slice(beforeCount);
1978
+ validatorViolations.forEach((v) => {
1979
+ const icon = v.type === 'error' || v.severity === 'critical' ? 'šŸ”“' :
1980
+ v.type === 'warning' || v.severity === 'high' ? '🟠' :
1981
+ v.severity === 'medium' ? '🟔' : '🟢';
1982
+ console.log(` ${icon} Line ${v.line || 'unknown'}: ${v.message}`);
1983
+ if (v.suggestion) {
1984
+ console.log(` šŸ’” ${v.suggestion}`);
1985
+ }
1986
+ });
1987
+ }
1988
+ }
1989
+ }
1990
+ // Convert context violations to standard format
1991
+ const standardViolations = context.violations.map((v) => ({
1992
+ rule: `${libraryName.toLowerCase()}-validator`,
1993
+ severity: v.severity || (v.type === 'error' ? 'critical' : v.type === 'warning' ? 'high' : 'medium'),
1994
+ line: v.line || 0,
1995
+ column: v.column || 0,
1996
+ message: v.message,
1997
+ code: v.code
1998
+ }));
1999
+ violations.push(...standardViolations);
2000
+ return violations;
2001
+ }
1545
2002
  }
1546
2003
  exports.ComponentLinter = ComponentLinter;
1547
2004
  // Universal rules that apply to all components with SavedUserSettings pattern
@@ -1733,6 +2190,201 @@ ComponentLinter.universalComponentRules = [
1733
2190
  return violations;
1734
2191
  }
1735
2192
  },
2193
+ {
2194
+ name: 'component-name-mismatch',
2195
+ appliesTo: 'all',
2196
+ test: (ast, componentName, componentSpec) => {
2197
+ const violations = [];
2198
+ // The expected component name from the spec
2199
+ const expectedName = componentSpec?.name || componentName;
2200
+ // Find the main function declaration
2201
+ let foundMainFunction = false;
2202
+ let actualFunctionName = null;
2203
+ (0, traverse_1.default)(ast, {
2204
+ FunctionDeclaration(path) {
2205
+ // Only check top-level function declarations
2206
+ if (path.parent === ast.program && path.node.id) {
2207
+ const funcName = path.node.id.name;
2208
+ // Check if this looks like the main component function
2209
+ // (starts with capital letter and has the typical props parameter)
2210
+ if (/^[A-Z]/.test(funcName)) {
2211
+ foundMainFunction = true;
2212
+ actualFunctionName = funcName;
2213
+ // Check if the function name matches the spec name
2214
+ if (funcName !== expectedName) {
2215
+ violations.push({
2216
+ rule: 'component-name-mismatch',
2217
+ severity: 'critical',
2218
+ line: path.node.loc?.start.line || 0,
2219
+ column: path.node.loc?.start.column || 0,
2220
+ message: `Component function name "${funcName}" does not match the spec name "${expectedName}". The function must be named exactly as specified in the component spec. Rename the function to: function ${expectedName}(...)`,
2221
+ code: `function ${funcName}(...)`
2222
+ });
2223
+ }
2224
+ // Also check that the first letter case matches
2225
+ const expectedFirstChar = expectedName.charAt(0);
2226
+ const actualFirstChar = funcName.charAt(0);
2227
+ if (expectedFirstChar !== actualFirstChar &&
2228
+ expectedName.toLowerCase() === funcName.toLowerCase()) {
2229
+ violations.push({
2230
+ rule: 'component-name-mismatch',
2231
+ severity: 'critical',
2232
+ line: path.node.loc?.start.line || 0,
2233
+ column: path.node.loc?.start.column || 0,
2234
+ message: `Component function name "${funcName}" has incorrect capitalization. Expected "${expectedName}" (note the case of the first letter). The function name must match exactly, including capitalization: function ${expectedName}(...)`,
2235
+ code: `function ${funcName}(...)`
2236
+ });
2237
+ }
2238
+ }
2239
+ }
2240
+ }
2241
+ });
2242
+ // If we didn't find a main function with the expected name
2243
+ if (!foundMainFunction && componentSpec?.name) {
2244
+ violations.push({
2245
+ rule: 'component-name-mismatch',
2246
+ severity: 'critical',
2247
+ line: 1,
2248
+ column: 0,
2249
+ message: `No function declaration found with the expected name "${expectedName}". The main component function must be named exactly as specified in the spec. Add a function declaration: function ${expectedName}({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) { ... }`
2250
+ });
2251
+ }
2252
+ return violations;
2253
+ }
2254
+ },
2255
+ {
2256
+ name: 'dependency-shadowing',
2257
+ appliesTo: 'all',
2258
+ test: (ast, componentName, componentSpec) => {
2259
+ const violations = [];
2260
+ // Get all dependency component names
2261
+ const dependencyNames = new Set();
2262
+ if (componentSpec?.dependencies) {
2263
+ for (const dep of componentSpec.dependencies) {
2264
+ if (dep.location === 'embedded' && dep.name) {
2265
+ dependencyNames.add(dep.name);
2266
+ }
2267
+ }
2268
+ }
2269
+ // If no dependencies, nothing to check
2270
+ if (dependencyNames.size === 0) {
2271
+ return violations;
2272
+ }
2273
+ // Find the main component function
2274
+ let mainComponentPath = null;
2275
+ (0, traverse_1.default)(ast, {
2276
+ FunctionDeclaration(path) {
2277
+ // Check if this is the main component function
2278
+ if (path.parent === ast.program &&
2279
+ path.node.id &&
2280
+ path.node.id.name === componentName) {
2281
+ mainComponentPath = path;
2282
+ path.stop();
2283
+ }
2284
+ }
2285
+ });
2286
+ if (!mainComponentPath) {
2287
+ return violations;
2288
+ }
2289
+ // Now traverse inside the main component to find shadowing definitions
2290
+ mainComponentPath.traverse({
2291
+ // Check for const/let/var ComponentName = ...
2292
+ VariableDeclarator(path) {
2293
+ if (t.isIdentifier(path.node.id)) {
2294
+ const varName = path.node.id.name;
2295
+ // Check if this shadows a dependency
2296
+ if (dependencyNames.has(varName)) {
2297
+ // Check if it's a function (component)
2298
+ const init = path.node.init;
2299
+ if (init && (t.isArrowFunctionExpression(init) ||
2300
+ t.isFunctionExpression(init))) {
2301
+ violations.push({
2302
+ rule: 'dependency-shadowing',
2303
+ severity: 'critical',
2304
+ line: path.node.loc?.start.line || 0,
2305
+ column: path.node.loc?.start.column || 0,
2306
+ message: `Component '${varName}' shadows a dependency component. The component '${varName}' is already available from dependencies (auto-destructured), but this code is creating a new definition which overrides it.`,
2307
+ code: `const ${varName} = ...`
2308
+ });
2309
+ }
2310
+ }
2311
+ }
2312
+ },
2313
+ // Check for function ComponentName() { ... }
2314
+ FunctionDeclaration(path) {
2315
+ if (path.node.id) {
2316
+ const funcName = path.node.id.name;
2317
+ // Check if this shadows a dependency
2318
+ if (dependencyNames.has(funcName)) {
2319
+ violations.push({
2320
+ rule: 'dependency-shadowing',
2321
+ severity: 'critical',
2322
+ line: path.node.loc?.start.line || 0,
2323
+ column: path.node.loc?.start.column || 0,
2324
+ message: `Component '${funcName}' shadows a dependency component. The component '${funcName}' is already available from dependencies (auto-destructured), but this code is creating a new function which overrides it.`,
2325
+ code: `function ${funcName}(...)`
2326
+ });
2327
+ }
2328
+ }
2329
+ }
2330
+ });
2331
+ // Components are now auto-destructured in the wrapper, so we don't need to check for manual destructuring
2332
+ // We just need to check if they're being used directly
2333
+ let hasComponentsUsage = false;
2334
+ const usedDependencies = new Set();
2335
+ mainComponentPath.traverse({
2336
+ // Look for direct usage of dependency components
2337
+ Identifier(path) {
2338
+ const name = path.node.name;
2339
+ if (dependencyNames.has(name)) {
2340
+ // Check if this is actually being used (not just in a declaration)
2341
+ if (path.isBindingIdentifier()) {
2342
+ return;
2343
+ }
2344
+ usedDependencies.add(name);
2345
+ hasComponentsUsage = true;
2346
+ }
2347
+ },
2348
+ // Still support legacy components.ComponentName usage
2349
+ MemberExpression(path) {
2350
+ if (t.isIdentifier(path.node.object) &&
2351
+ path.node.object.name === 'components' &&
2352
+ t.isIdentifier(path.node.property)) {
2353
+ const name = path.node.property.name;
2354
+ if (dependencyNames.has(name)) {
2355
+ usedDependencies.add(name);
2356
+ hasComponentsUsage = true;
2357
+ }
2358
+ }
2359
+ },
2360
+ // Also look in JSX elements
2361
+ JSXMemberExpression(path) {
2362
+ if (t.isJSXIdentifier(path.node.object) &&
2363
+ path.node.object.name === 'components' &&
2364
+ t.isJSXIdentifier(path.node.property)) {
2365
+ const name = path.node.property.name;
2366
+ if (dependencyNames.has(name)) {
2367
+ usedDependencies.add(name);
2368
+ hasComponentsUsage = true; // Mark as properly accessed
2369
+ }
2370
+ }
2371
+ }
2372
+ });
2373
+ // Components are now auto-destructured, so just check for unused dependencies
2374
+ if (dependencyNames.size > 0 && usedDependencies.size === 0) {
2375
+ const depList = Array.from(dependencyNames).join(', ');
2376
+ violations.push({
2377
+ rule: 'dependency-shadowing',
2378
+ severity: 'low',
2379
+ line: mainComponentPath.node.loc?.start.line || 0,
2380
+ column: mainComponentPath.node.loc?.start.column || 0,
2381
+ message: `Component has dependencies [${depList}] defined in spec but they're not being used. These components are available for use.`,
2382
+ code: `// Available: ${depList}`
2383
+ });
2384
+ }
2385
+ return violations;
2386
+ }
2387
+ },
1736
2388
  {
1737
2389
  name: 'no-window-access',
1738
2390
  appliesTo: 'all',
@@ -1957,10 +2609,10 @@ ComponentLinter.universalComponentRules = [
1957
2609
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1958
2610
  violations.push({
1959
2611
  rule: 'no-data-prop',
1960
- severity: 'medium', // It's a pattern issue, not critical
2612
+ severity: 'low', // Opinion-based style preference, not a functional issue
1961
2613
  line: prop.loc?.start.line || 0,
1962
2614
  column: prop.loc?.start.column || 0,
1963
- message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
2615
+ message: `Component "${componentName}" accepts generic 'data' prop. Consider using more specific prop names like 'items', 'customers', etc. for clarity.`,
1964
2616
  code: 'data prop in component signature'
1965
2617
  });
1966
2618
  }
@@ -1979,10 +2631,10 @@ ComponentLinter.universalComponentRules = [
1979
2631
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
1980
2632
  violations.push({
1981
2633
  rule: 'no-data-prop',
1982
- severity: 'critical',
2634
+ severity: 'low', // Opinion-based style preference, not a functional issue
1983
2635
  line: prop.loc?.start.line || 0,
1984
2636
  column: prop.loc?.start.column || 0,
1985
- message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
2637
+ message: `Component "${componentName}" accepts generic 'data' prop. Consider using more specific prop names like 'items', 'customers', etc. for clarity.`,
1986
2638
  code: 'data prop in component signature'
1987
2639
  });
1988
2640
  }
@@ -2253,83 +2905,95 @@ ComponentLinter.universalComponentRules = [
2253
2905
  return violations;
2254
2906
  }
2255
2907
  },
2256
- {
2257
- name: 'unsafe-array-access',
2258
- appliesTo: 'all',
2259
- test: (ast, componentName, componentSpec) => {
2260
- const violations = [];
2261
- (0, traverse_1.default)(ast, {
2262
- MemberExpression(path) {
2263
- // Check for array[index] patterns
2264
- if (t.isNumericLiteral(path.node.property) ||
2265
- (t.isIdentifier(path.node.property) && path.node.computed && /^\d+$/.test(path.node.property.name))) {
2266
- // Look for patterns like: someArray[0].method()
2267
- const parent = path.parent;
2268
- if (t.isMemberExpression(parent) && parent.object === path.node) {
2269
- const code = path.toString();
2270
- // Check if it's an array access followed by a method call
2271
- if (/\[\d+\]\.\w+/.test(code)) {
2272
- violations.push({
2273
- rule: 'unsafe-array-access',
2274
- severity: 'critical',
2275
- line: path.node.loc?.start.line || 0,
2276
- column: path.node.loc?.start.column || 0,
2277
- message: `Unsafe array access: ${code}. Check array bounds before accessing elements.`,
2278
- code: code
2279
- });
2280
- }
2281
- }
2282
- }
2283
- }
2284
- });
2285
- return violations;
2286
- }
2287
- },
2288
- {
2289
- name: 'array-reduce-safety',
2290
- appliesTo: 'all',
2291
- test: (ast, componentName, componentSpec) => {
2292
- const violations = [];
2293
- (0, traverse_1.default)(ast, {
2294
- CallExpression(path) {
2295
- // Check for .reduce() calls
2296
- if (t.isMemberExpression(path.node.callee) &&
2297
- t.isIdentifier(path.node.callee.property) &&
2298
- path.node.callee.property.name === 'reduce') {
2299
- // Check if the array might be empty
2300
- const arrayExpression = path.node.callee.object;
2301
- const code = path.toString();
2302
- // Look for patterns that suggest no safety check
2303
- const hasInitialValue = path.node.arguments.length > 1;
2304
- if (!hasInitialValue) {
2305
- violations.push({
2306
- rule: 'array-reduce-safety',
2307
- severity: 'low',
2308
- line: path.node.loc?.start.line || 0,
2309
- column: path.node.loc?.start.column || 0,
2310
- message: `reduce() without initial value may fail on empty arrays: ${code}`,
2311
- code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
2312
- });
2313
- }
2314
- // Check for reduce on array access like arr[0].reduce()
2315
- if (t.isMemberExpression(arrayExpression) &&
2316
- (t.isNumericLiteral(arrayExpression.property) ||
2317
- (t.isIdentifier(arrayExpression.property) && arrayExpression.computed))) {
2318
- violations.push({
2319
- rule: 'array-reduce-safety',
2320
- severity: 'critical',
2321
- line: path.node.loc?.start.line || 0,
2322
- column: path.node.loc?.start.column || 0,
2323
- message: `reduce() on array element access is unsafe: ${code}`,
2324
- code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
2325
- });
2326
- }
2327
- }
2328
- }
2329
- });
2330
- return violations;
2331
- }
2332
- },
2908
+ // DISABLED: Consolidated into unsafe-array-operations rule
2909
+ // {
2910
+ // name: 'unsafe-array-access',
2911
+ // appliesTo: 'all',
2912
+ // test: (ast: t.File, componentName: string, componentSpec?: ComponentSpec) => {
2913
+ // const violations: Violation[] = [];
2914
+ //
2915
+ // traverse(ast, {
2916
+ // MemberExpression(path: NodePath<t.MemberExpression>) {
2917
+ // // Check for array[index] patterns
2918
+ // if (t.isNumericLiteral(path.node.property) ||
2919
+ // (t.isIdentifier(path.node.property) && path.node.computed && /^\d+$/.test(path.node.property.name))) {
2920
+ //
2921
+ // // Look for patterns like: someArray[0].method()
2922
+ // const parent = path.parent;
2923
+ // if (t.isMemberExpression(parent) && parent.object === path.node) {
2924
+ // const code = path.toString();
2925
+ //
2926
+ // // Check if it's an array access followed by a method call
2927
+ // if (/\[\d+\]\.\w+/.test(code)) {
2928
+ // violations.push({
2929
+ // rule: 'unsafe-array-access',
2930
+ // severity: 'critical',
2931
+ // line: path.node.loc?.start.line || 0,
2932
+ // column: path.node.loc?.start.column || 0,
2933
+ // message: `Unsafe array access: ${code}. Check array bounds before accessing elements.`,
2934
+ // code: code
2935
+ // });
2936
+ // }
2937
+ // }
2938
+ // }
2939
+ // }
2940
+ // });
2941
+ //
2942
+ // return violations;
2943
+ // }
2944
+ // },
2945
+ // DISABLED: Consolidated into unsafe-array-operations rule
2946
+ // {
2947
+ // name: 'array-reduce-safety',
2948
+ // appliesTo: 'all',
2949
+ // test: (ast: t.File, componentName: string, componentSpec?: ComponentSpec) => {
2950
+ // const violations: Violation[] = [];
2951
+ //
2952
+ // traverse(ast, {
2953
+ // CallExpression(path: NodePath<t.CallExpression>) {
2954
+ // // Check for .reduce() calls
2955
+ // if (t.isMemberExpression(path.node.callee) &&
2956
+ // t.isIdentifier(path.node.callee.property) &&
2957
+ // path.node.callee.property.name === 'reduce') {
2958
+ //
2959
+ // // Check if the array might be empty
2960
+ // const arrayExpression = path.node.callee.object;
2961
+ // const code = path.toString();
2962
+ //
2963
+ // // Look for patterns that suggest no safety check
2964
+ // const hasInitialValue = path.node.arguments.length > 1;
2965
+ //
2966
+ // if (!hasInitialValue) {
2967
+ // violations.push({
2968
+ // rule: 'array-reduce-safety',
2969
+ // severity: 'low',
2970
+ // line: path.node.loc?.start.line || 0,
2971
+ // column: path.node.loc?.start.column || 0,
2972
+ // message: `reduce() without initial value may fail on empty arrays: ${code}`,
2973
+ // code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
2974
+ // });
2975
+ // }
2976
+ //
2977
+ // // Check for reduce on array access like arr[0].reduce()
2978
+ // if (t.isMemberExpression(arrayExpression) &&
2979
+ // (t.isNumericLiteral(arrayExpression.property) ||
2980
+ // (t.isIdentifier(arrayExpression.property) && arrayExpression.computed))) {
2981
+ // violations.push({
2982
+ // rule: 'array-reduce-safety',
2983
+ // severity: 'critical',
2984
+ // line: path.node.loc?.start.line || 0,
2985
+ // column: path.node.loc?.start.column || 0,
2986
+ // message: `reduce() on array element access is unsafe: ${code}`,
2987
+ // code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
2988
+ // });
2989
+ // }
2990
+ // }
2991
+ // }
2992
+ // });
2993
+ //
2994
+ // return violations;
2995
+ // }
2996
+ // },
2333
2997
  // {
2334
2998
  // name: 'parent-event-callback-usage',
2335
2999
  // appliesTo: 'child',
@@ -2657,116 +3321,242 @@ ComponentLinter.universalComponentRules = [
2657
3321
  return violations;
2658
3322
  }
2659
3323
  },
3324
+ // DISABLED: Too aggressive - not all array operations need memoization
3325
+ // {
3326
+ // name: 'performance-memoization',
3327
+ // appliesTo: 'all',
3328
+ // test: (ast: t.File, componentName: string, componentSpec?: ComponentSpec) => {
3329
+ // const violations: Violation[] = [];
3330
+ // const memoizedValues = new Set<string>();
3331
+ //
3332
+ // // Collect memoized values
3333
+ // traverse(ast, {
3334
+ // CallExpression(path: NodePath<t.CallExpression>) {
3335
+ // if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useMemo') {
3336
+ // // Find the variable being assigned
3337
+ // if (t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) {
3338
+ // memoizedValues.add(path.parent.id.name);
3339
+ // }
3340
+ // }
3341
+ // }
3342
+ // });
3343
+ //
3344
+ // // Check for expensive operations without memoization
3345
+ // traverse(ast, {
3346
+ // CallExpression(path: NodePath<t.CallExpression>) {
3347
+ // if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
3348
+ // const method = path.node.callee.property.name;
3349
+ //
3350
+ // // Check for expensive array operations
3351
+ // if (['filter', 'sort', 'map', 'reduce'].includes(method)) {
3352
+ // // Check if this is inside a variable declaration
3353
+ // let parentPath: NodePath | null = path.parentPath;
3354
+ // while (parentPath && !t.isVariableDeclarator(parentPath.node)) {
3355
+ // parentPath = parentPath.parentPath;
3356
+ // }
3357
+ //
3358
+ // if (parentPath && t.isVariableDeclarator(parentPath.node) && t.isIdentifier(parentPath.node.id)) {
3359
+ // const varName = parentPath.node.id.name;
3360
+ //
3361
+ // // Check if it's not memoized
3362
+ // if (!memoizedValues.has(varName)) {
3363
+ // // Check if it's in the render method (not in event handlers)
3364
+ // let funcParent = path.getFunctionParent();
3365
+ // if (funcParent) {
3366
+ // const funcName = ComponentLinter.getFunctionName(funcParent);
3367
+ // if (!funcName || funcName === componentName) {
3368
+ // violations.push({
3369
+ // rule: 'performance-memoization',
3370
+ // severity: 'low', // Just a suggestion, not mandatory
3371
+ // line: path.node.loc?.start.line || 0,
3372
+ // column: path.node.loc?.start.column || 0,
3373
+ // message: `Expensive ${method} operation without memoization. Consider using useMemo.`,
3374
+ // code: `const ${varName} = ...${method}(...)`
3375
+ // });
3376
+ // }
3377
+ // }
3378
+ // }
3379
+ // }
3380
+ // }
3381
+ // }
3382
+ // },
3383
+ //
3384
+ // // Check for static arrays/objects
3385
+ // VariableDeclarator(path: NodePath<t.VariableDeclarator>) {
3386
+ // if (t.isIdentifier(path.node.id) &&
3387
+ // (t.isArrayExpression(path.node.init) || t.isObjectExpression(path.node.init))) {
3388
+ //
3389
+ // const varName = path.node.id.name;
3390
+ // if (!memoizedValues.has(varName)) {
3391
+ // // Check if it looks static (no variables referenced)
3392
+ // const hasVariables = path.node.init.toString().match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g);
3393
+ // if (!hasVariables || hasVariables.length < 3) { // Allow some property names
3394
+ // violations.push({
3395
+ // rule: 'performance-memoization',
3396
+ // severity: 'low', // Just a suggestion
3397
+ // line: path.node.loc?.start.line || 0,
3398
+ // column: path.node.loc?.start.column || 0,
3399
+ // message: 'Static array/object recreated on every render. Consider using useMemo.',
3400
+ // code: `const ${varName} = ${path.node.init.type === 'ArrayExpression' ? '[...]' : '{...}'}`
3401
+ // });
3402
+ // }
3403
+ // }
3404
+ // }
3405
+ // }
3406
+ // });
3407
+ //
3408
+ // return violations;
3409
+ // }
3410
+ // },
2660
3411
  {
2661
- name: 'performance-memoization',
3412
+ name: 'react-hooks-rules',
2662
3413
  appliesTo: 'all',
2663
3414
  test: (ast, componentName, componentSpec) => {
2664
3415
  const violations = [];
2665
- const memoizedValues = new Set();
2666
- // Collect memoized values
3416
+ const hooks = ['useState', 'useEffect', 'useMemo', 'useCallback', 'useRef', 'useContext', 'useReducer', 'useLayoutEffect'];
2667
3417
  (0, traverse_1.default)(ast, {
2668
3418
  CallExpression(path) {
2669
- if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useMemo') {
2670
- // Find the variable being assigned
2671
- if (t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) {
2672
- memoizedValues.add(path.parent.id.name);
3419
+ if (t.isIdentifier(path.node.callee) && hooks.includes(path.node.callee.name)) {
3420
+ const hookName = path.node.callee.name;
3421
+ // Rule 1: Check if hook is inside the main component function or custom hook
3422
+ let funcParent = path.getFunctionParent();
3423
+ if (funcParent) {
3424
+ const funcName = ComponentLinter.getFunctionName(funcParent);
3425
+ // Violation: Hook not in component or custom hook
3426
+ if (funcName && funcName !== componentName && !funcName.startsWith('use')) {
3427
+ violations.push({
3428
+ rule: 'react-hooks-rules',
3429
+ severity: 'critical',
3430
+ line: path.node.loc?.start.line || 0,
3431
+ column: path.node.loc?.start.column || 0,
3432
+ message: `React Hook "${hookName}" cannot be called inside function "${funcName}". Hooks can only be called at the top level of React components or custom hooks.`,
3433
+ code: path.toString().substring(0, 100)
3434
+ });
3435
+ return; // Skip further checks for this hook
3436
+ }
2673
3437
  }
2674
- }
2675
- }
2676
- });
2677
- // Check for expensive operations without memoization
2678
- (0, traverse_1.default)(ast, {
2679
- CallExpression(path) {
2680
- if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
2681
- const method = path.node.callee.property.name;
2682
- // Check for expensive array operations
2683
- if (['filter', 'sort', 'map', 'reduce'].includes(method)) {
2684
- // Check if this is inside a variable declaration
2685
- let parentPath = path.parentPath;
2686
- while (parentPath && !t.isVariableDeclarator(parentPath.node)) {
2687
- parentPath = parentPath.parentPath;
2688
- }
2689
- if (parentPath && t.isVariableDeclarator(parentPath.node) && t.isIdentifier(parentPath.node.id)) {
2690
- const varName = parentPath.node.id.name;
2691
- // Check if it's not memoized
2692
- if (!memoizedValues.has(varName)) {
2693
- // Check if it's in the render method (not in event handlers)
2694
- let funcParent = path.getFunctionParent();
2695
- if (funcParent) {
2696
- const funcName = ComponentLinter.getFunctionName(funcParent);
2697
- if (!funcName || funcName === componentName) {
2698
- violations.push({
2699
- rule: 'performance-memoization',
2700
- severity: 'low', // Just a suggestion, not mandatory
2701
- line: path.node.loc?.start.line || 0,
2702
- column: path.node.loc?.start.column || 0,
2703
- message: `Expensive ${method} operation without memoization. Consider using useMemo.`,
2704
- code: `const ${varName} = ...${method}(...)`
2705
- });
2706
- }
2707
- }
3438
+ // Rule 2: Check if hook is inside a conditional (if statement)
3439
+ let parent = path.parentPath;
3440
+ while (parent) {
3441
+ // Check if we've reached the component function - stop looking
3442
+ if (t.isFunctionDeclaration(parent.node) ||
3443
+ t.isFunctionExpression(parent.node) ||
3444
+ t.isArrowFunctionExpression(parent.node)) {
3445
+ const parentFuncName = ComponentLinter.getFunctionName(parent);
3446
+ if (parentFuncName === componentName || parentFuncName?.startsWith('use')) {
3447
+ break; // We've reached the component/hook boundary
2708
3448
  }
2709
3449
  }
2710
- }
2711
- }
2712
- },
2713
- // Check for static arrays/objects
2714
- VariableDeclarator(path) {
2715
- if (t.isIdentifier(path.node.id) &&
2716
- (t.isArrayExpression(path.node.init) || t.isObjectExpression(path.node.init))) {
2717
- const varName = path.node.id.name;
2718
- if (!memoizedValues.has(varName)) {
2719
- // Check if it looks static (no variables referenced)
2720
- const hasVariables = path.node.init.toString().match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g);
2721
- if (!hasVariables || hasVariables.length < 3) { // Allow some property names
3450
+ // Check for conditional statements
3451
+ if (t.isIfStatement(parent.node)) {
2722
3452
  violations.push({
2723
- rule: 'performance-memoization',
2724
- severity: 'low', // Just a suggestion
3453
+ rule: 'react-hooks-rules',
3454
+ severity: 'critical',
2725
3455
  line: path.node.loc?.start.line || 0,
2726
3456
  column: path.node.loc?.start.column || 0,
2727
- message: 'Static array/object recreated on every render. Consider using useMemo.',
2728
- code: `const ${varName} = ${path.node.init.type === 'ArrayExpression' ? '[...]' : '{...}'}`
3457
+ message: `React Hook "${hookName}" is called conditionally. Hooks must be called in the exact same order in every component render.`,
3458
+ code: path.toString().substring(0, 100)
2729
3459
  });
3460
+ break;
2730
3461
  }
2731
- }
2732
- }
2733
- }
2734
- });
2735
- return violations;
2736
- }
2737
- },
2738
- {
2739
- name: 'child-state-management',
2740
- appliesTo: 'all',
2741
- test: (ast, componentName, componentSpec) => {
2742
- const violations = [];
2743
- (0, traverse_1.default)(ast, {
2744
- CallExpression(path) {
2745
- if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useState') {
2746
- // Check if the state name suggests child component state
2747
- if (t.isVariableDeclarator(path.parent) && t.isArrayPattern(path.parent.id)) {
2748
- const stateNameNode = path.parent.id.elements[0];
2749
- if (t.isIdentifier(stateNameNode)) {
2750
- const stateName = stateNameNode.name;
2751
- // Check for patterns suggesting child state management
2752
- const childPatterns = [
2753
- /^child/i,
2754
- /Table\w*State/,
2755
- /Panel\w*State/,
2756
- /Modal\w*State/,
2757
- /\w+Component\w*/
2758
- ];
2759
- if (childPatterns.some(pattern => pattern.test(stateName))) {
2760
- violations.push({
2761
- rule: 'child-state-management',
2762
- severity: 'critical',
2763
- line: path.node.loc?.start.line || 0,
2764
- column: path.node.loc?.start.column || 0,
2765
- message: `Component trying to manage child component state: ${stateName}. Child components manage their own state!`,
2766
- code: `const [${stateName}, ...] = useState(...)`
2767
- });
3462
+ // Check for ternary expressions
3463
+ if (t.isConditionalExpression(parent.node)) {
3464
+ violations.push({
3465
+ rule: 'react-hooks-rules',
3466
+ severity: 'critical',
3467
+ line: path.node.loc?.start.line || 0,
3468
+ column: path.node.loc?.start.column || 0,
3469
+ message: `React Hook "${hookName}" is called conditionally in a ternary expression. Hooks must be called unconditionally.`,
3470
+ code: path.toString().substring(0, 100)
3471
+ });
3472
+ break;
3473
+ }
3474
+ // Check for logical expressions (&&, ||)
3475
+ if (t.isLogicalExpression(parent.node)) {
3476
+ violations.push({
3477
+ rule: 'react-hooks-rules',
3478
+ severity: 'critical',
3479
+ line: path.node.loc?.start.line || 0,
3480
+ column: path.node.loc?.start.column || 0,
3481
+ message: `React Hook "${hookName}" is called conditionally in a logical expression. Hooks must be called unconditionally.`,
3482
+ code: path.toString().substring(0, 100)
3483
+ });
3484
+ break;
3485
+ }
3486
+ // Check for switch statements
3487
+ if (t.isSwitchStatement(parent.node) || t.isSwitchCase(parent.node)) {
3488
+ violations.push({
3489
+ rule: 'react-hooks-rules',
3490
+ severity: 'critical',
3491
+ line: path.node.loc?.start.line || 0,
3492
+ column: path.node.loc?.start.column || 0,
3493
+ message: `React Hook "${hookName}" is called inside a switch statement. Hooks must be called at the top level.`,
3494
+ code: path.toString().substring(0, 100)
3495
+ });
3496
+ break;
3497
+ }
3498
+ // Rule 3: Check for loops
3499
+ if (t.isForStatement(parent.node) ||
3500
+ t.isForInStatement(parent.node) ||
3501
+ t.isForOfStatement(parent.node) ||
3502
+ t.isWhileStatement(parent.node) ||
3503
+ t.isDoWhileStatement(parent.node)) {
3504
+ violations.push({
3505
+ rule: 'react-hooks-rules',
3506
+ severity: 'critical',
3507
+ line: path.node.loc?.start.line || 0,
3508
+ column: path.node.loc?.start.column || 0,
3509
+ message: `React Hook "${hookName}" may not be called inside a loop. This can lead to hooks being called in different order between renders.`,
3510
+ code: path.toString().substring(0, 100)
3511
+ });
3512
+ break;
3513
+ }
3514
+ // Rule 4: Check for try/catch blocks
3515
+ if (t.isTryStatement(parent.node) || t.isCatchClause(parent.node)) {
3516
+ violations.push({
3517
+ rule: 'react-hooks-rules',
3518
+ severity: 'high', // Less severe as it might be intentional
3519
+ line: path.node.loc?.start.line || 0,
3520
+ column: path.node.loc?.start.column || 0,
3521
+ message: `React Hook "${hookName}" is called inside a try/catch block. While not strictly forbidden, this can lead to issues if the hook throws.`,
3522
+ code: path.toString().substring(0, 100)
3523
+ });
3524
+ break;
3525
+ }
3526
+ // Rule 5: Check for early returns before this hook
3527
+ // This is complex and would need to track control flow, so we'll do a simpler check
3528
+ if (t.isBlockStatement(parent.node)) {
3529
+ const statements = parent.node.body;
3530
+ const hookIndex = statements.findIndex(stmt => stmt === path.parentPath?.node);
3531
+ // Check if there's a return statement before this hook
3532
+ for (let i = 0; i < hookIndex; i++) {
3533
+ const stmt = statements[i];
3534
+ if (t.isReturnStatement(stmt)) {
3535
+ violations.push({
3536
+ rule: 'react-hooks-rules',
3537
+ severity: 'critical',
3538
+ line: path.node.loc?.start.line || 0,
3539
+ column: path.node.loc?.start.column || 0,
3540
+ message: `React Hook "${hookName}" is called after a conditional early return. All hooks must be called before any conditional returns.`,
3541
+ code: path.toString().substring(0, 100)
3542
+ });
3543
+ break;
3544
+ }
3545
+ // Check for conditional returns
3546
+ if (t.isIfStatement(stmt) && ComponentLinter.containsReturn(stmt)) {
3547
+ violations.push({
3548
+ rule: 'react-hooks-rules',
3549
+ severity: 'critical',
3550
+ line: path.node.loc?.start.line || 0,
3551
+ column: path.node.loc?.start.column || 0,
3552
+ message: `React Hook "${hookName}" is called after a possible early return. Move this hook before any conditional logic.`,
3553
+ code: path.toString().substring(0, 100)
3554
+ });
3555
+ break;
3556
+ }
2768
3557
  }
2769
3558
  }
3559
+ parent = parent.parentPath;
2770
3560
  }
2771
3561
  }
2772
3562
  }
@@ -2774,6 +3564,50 @@ ComponentLinter.universalComponentRules = [
2774
3564
  return violations;
2775
3565
  }
2776
3566
  },
3567
+ // DISABLED: Too aggressive - flags legitimate state based on naming patterns
3568
+ // {
3569
+ // name: 'child-state-management',
3570
+ // appliesTo: 'all',
3571
+ // test: (ast: t.File, componentName: string, componentSpec?: ComponentSpec) => {
3572
+ // const violations: Violation[] = [];
3573
+ //
3574
+ // traverse(ast, {
3575
+ // CallExpression(path: NodePath<t.CallExpression>) {
3576
+ // if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useState') {
3577
+ // // Check if the state name suggests child component state
3578
+ // if (t.isVariableDeclarator(path.parent) && t.isArrayPattern(path.parent.id)) {
3579
+ // const stateNameNode = path.parent.id.elements[0];
3580
+ // if (t.isIdentifier(stateNameNode)) {
3581
+ // const stateName = stateNameNode.name;
3582
+ //
3583
+ // // Check for patterns suggesting child state management
3584
+ // const childPatterns = [
3585
+ // /^child/i,
3586
+ // /Table\w*State/,
3587
+ // /Panel\w*State/,
3588
+ // /Modal\w*State/,
3589
+ // /\w+Component\w*/
3590
+ // ];
3591
+ //
3592
+ // if (childPatterns.some(pattern => pattern.test(stateName))) {
3593
+ // violations.push({
3594
+ // rule: 'child-state-management',
3595
+ // severity: 'critical',
3596
+ // line: path.node.loc?.start.line || 0,
3597
+ // column: path.node.loc?.start.column || 0,
3598
+ // message: `Component trying to manage child component state: ${stateName}. Child components manage their own state!`,
3599
+ // code: `const [${stateName}, ...] = useState(...)`
3600
+ // });
3601
+ // }
3602
+ // }
3603
+ // }
3604
+ // }
3605
+ // }
3606
+ // });
3607
+ //
3608
+ // return violations;
3609
+ // }
3610
+ // },
2777
3611
  {
2778
3612
  name: 'server-reload-on-client-operation',
2779
3613
  appliesTo: 'all',
@@ -3043,69 +3877,591 @@ ComponentLinter.universalComponentRules = [
3043
3877
  }
3044
3878
  },
3045
3879
  {
3046
- name: 'root-component-props-restriction',
3047
- appliesTo: 'root',
3880
+ name: 'runquery-parameters-validation',
3881
+ appliesTo: 'all',
3048
3882
  test: (ast, componentName, componentSpec) => {
3049
3883
  const violations = [];
3050
- const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
3051
- // This rule applies when testing root components
3052
- // We can identify this by checking if the component spec indicates it's a root component
3053
- // For now, we'll apply this rule universally and let the caller decide when to use it
3054
3884
  (0, traverse_1.default)(ast, {
3055
- FunctionDeclaration(path) {
3056
- if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
3057
- const param = path.node.params[0];
3058
- if (t.isObjectPattern(param)) {
3059
- const invalidProps = [];
3060
- const allProps = [];
3061
- for (const prop of param.properties) {
3062
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3063
- const propName = prop.key.name;
3064
- allProps.push(propName);
3065
- if (!standardProps.has(propName)) {
3066
- invalidProps.push(propName);
3067
- }
3885
+ CallExpression(path) {
3886
+ const callee = path.node.callee;
3887
+ // Check for utilities.rq.RunQuery
3888
+ if (t.isMemberExpression(callee) &&
3889
+ t.isMemberExpression(callee.object) &&
3890
+ t.isIdentifier(callee.object.object) &&
3891
+ callee.object.object.name === 'utilities' &&
3892
+ t.isIdentifier(callee.object.property) &&
3893
+ callee.object.property.name === 'rq' &&
3894
+ t.isIdentifier(callee.property) &&
3895
+ callee.property.name === 'RunQuery') {
3896
+ // Get the first argument (RunQuery params object)
3897
+ const runQueryParams = path.node.arguments[0];
3898
+ if (!t.isObjectExpression(runQueryParams))
3899
+ return;
3900
+ // Find QueryName or QueryID to identify the query
3901
+ let queryName = null;
3902
+ let parametersNode = null;
3903
+ for (const prop of runQueryParams.properties) {
3904
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3905
+ if (prop.key.name === 'QueryName' && t.isStringLiteral(prop.value)) {
3906
+ queryName = prop.value.value;
3907
+ }
3908
+ else if (prop.key.name === 'Parameters') {
3909
+ parametersNode = prop;
3068
3910
  }
3069
- }
3070
- // Only report if there are non-standard props
3071
- // This allows the rule to be selectively applied to root components
3072
- if (invalidProps.length > 0) {
3073
- violations.push({
3074
- rule: 'root-component-props-restriction',
3075
- severity: 'critical',
3076
- line: path.node.loc?.start.line || 0,
3077
- column: path.node.loc?.start.column || 0,
3078
- message: `Component "${componentName}" accepts non-standard props: ${invalidProps.join(', ')}. Root components can only accept standard props: ${Array.from(standardProps).join(', ')}. Load data internally using utilities.rv.RunView().`
3079
- });
3080
3911
  }
3081
3912
  }
3082
- }
3083
- },
3084
- // Also check arrow function components
3085
- VariableDeclarator(path) {
3086
- if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
3087
- const init = path.node.init;
3088
- if (t.isArrowFunctionExpression(init) && init.params[0]) {
3089
- const param = init.params[0];
3090
- if (t.isObjectPattern(param)) {
3091
- const invalidProps = [];
3092
- const allProps = [];
3093
- for (const prop of param.properties) {
3913
+ // Skip if no Parameters property
3914
+ if (!parametersNode)
3915
+ return;
3916
+ // Find the query in componentSpec if available
3917
+ let specQuery;
3918
+ if (componentSpec?.dataRequirements?.queries && queryName) {
3919
+ specQuery = componentSpec.dataRequirements.queries.find(q => q.name === queryName);
3920
+ }
3921
+ // Validate Parameters structure
3922
+ const paramValue = parametersNode.value;
3923
+ // Case 1: Parameters is an array (incorrect format)
3924
+ if (t.isArrayExpression(paramValue)) {
3925
+ const arrayElements = paramValue.elements.filter((e) => t.isObjectExpression(e));
3926
+ // Check if it's an array of {Name/FieldName, Value} objects
3927
+ const paramPairs = [];
3928
+ let isNameValueFormat = true;
3929
+ for (const elem of arrayElements) {
3930
+ let name = null;
3931
+ let value = null;
3932
+ for (const prop of elem.properties) {
3094
3933
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3095
- const propName = prop.key.name;
3096
- allProps.push(propName);
3097
- if (!standardProps.has(propName)) {
3098
- invalidProps.push(propName);
3934
+ const propName = prop.key.name.toLowerCase();
3935
+ if (propName === 'name' || propName === 'fieldname') {
3936
+ if (t.isStringLiteral(prop.value)) {
3937
+ name = prop.value.value;
3938
+ }
3939
+ else if (t.isIdentifier(prop.value)) {
3940
+ name = prop.value.name;
3941
+ }
3942
+ }
3943
+ else if (propName === 'value') {
3944
+ // Get the actual value (could be string, number, boolean, etc.)
3945
+ if (t.isStringLiteral(prop.value)) {
3946
+ value = `'${prop.value.value}'`;
3947
+ }
3948
+ else if (t.isNumericLiteral(prop.value)) {
3949
+ value = prop.value.value;
3950
+ }
3951
+ else if (t.isBooleanLiteral(prop.value)) {
3952
+ value = prop.value.value;
3953
+ }
3954
+ else if (t.isIdentifier(prop.value)) {
3955
+ value = prop.value.name;
3956
+ }
3957
+ else {
3958
+ value = '/* value */';
3959
+ }
3099
3960
  }
3100
3961
  }
3101
3962
  }
3102
- if (invalidProps.length > 0) {
3103
- violations.push({
3104
- rule: 'root-component-props-restriction',
3963
+ if (name && value !== null) {
3964
+ paramPairs.push({ name, value });
3965
+ }
3966
+ else {
3967
+ isNameValueFormat = false;
3968
+ break;
3969
+ }
3970
+ }
3971
+ // Generate fix suggestion
3972
+ let fixMessage;
3973
+ let fixCode;
3974
+ if (isNameValueFormat && paramPairs.length > 0) {
3975
+ // Convert array format to object
3976
+ const objProps = paramPairs.map(p => ` ${p.name}: ${p.value}`).join(',\n');
3977
+ fixCode = `Parameters: {\n${objProps}\n}`;
3978
+ // Check against spec if available
3979
+ if (specQuery?.parameters) {
3980
+ const specParamNames = specQuery.parameters.map(p => p.name);
3981
+ const providedNames = paramPairs.map(p => p.name);
3982
+ const missing = specParamNames.filter(n => !providedNames.includes(n));
3983
+ const extra = providedNames.filter(n => !specParamNames.includes(n));
3984
+ if (missing.length > 0 || extra.length > 0) {
3985
+ fixMessage = `RunQuery Parameters must be object, not array. `;
3986
+ if (missing.length > 0) {
3987
+ fixMessage += `Missing required: ${missing.join(', ')}. `;
3988
+ }
3989
+ if (extra.length > 0) {
3990
+ fixMessage += `Unknown params: ${extra.join(', ')}. `;
3991
+ }
3992
+ fixMessage += `Expected params from spec: ${specParamNames.join(', ')}`;
3993
+ }
3994
+ else {
3995
+ fixMessage = `RunQuery Parameters must be object with key-value pairs, not array. Auto-fix: convert [{Name,Value}] to object format`;
3996
+ }
3997
+ }
3998
+ else {
3999
+ fixMessage = `RunQuery Parameters must be object with key-value pairs, not array of {Name/Value} objects`;
4000
+ }
4001
+ }
4002
+ else {
4003
+ // Invalid array format - provide example
4004
+ if (specQuery?.parameters && specQuery.parameters.length > 0) {
4005
+ const exampleParams = specQuery.parameters
4006
+ .slice(0, 3)
4007
+ .map(p => ` ${p.name}: '${p.testValue || 'value'}'`)
4008
+ .join(',\n');
4009
+ fixCode = `Parameters: {\n${exampleParams}\n}`;
4010
+ fixMessage = `RunQuery Parameters must be object. Expected params: ${specQuery.parameters.map(p => p.name).join(', ')}`;
4011
+ }
4012
+ else {
4013
+ fixCode = `Parameters: {\n paramName1: 'value1',\n paramName2: 'value2'\n}`;
4014
+ fixMessage = `RunQuery Parameters must be object with key-value pairs, not array`;
4015
+ }
4016
+ }
4017
+ violations.push({
4018
+ rule: 'runquery-parameters-validation',
4019
+ severity: 'critical',
4020
+ line: parametersNode.loc?.start.line || 0,
4021
+ column: parametersNode.loc?.start.column || 0,
4022
+ message: fixMessage,
4023
+ code: fixCode
4024
+ });
4025
+ }
4026
+ // Case 2: Parameters is an object (correct format, but validate against spec)
4027
+ else if (t.isObjectExpression(paramValue) && specQuery?.parameters) {
4028
+ // Create maps for case-insensitive comparison
4029
+ const providedParamsMap = new Map(); // lowercase -> original
4030
+ for (const prop of paramValue.properties) {
4031
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
4032
+ providedParamsMap.set(prop.key.name.toLowerCase(), prop.key.name);
4033
+ }
4034
+ }
4035
+ const specParamNames = specQuery.parameters.map(p => p.name);
4036
+ const specParamNamesLower = specParamNames.map(n => n.toLowerCase());
4037
+ // Find missing parameters (case-insensitive)
4038
+ const missing = specParamNames.filter(n => !providedParamsMap.has(n.toLowerCase()));
4039
+ // Find extra parameters (not matching any spec param case-insensitively)
4040
+ const extra = Array.from(providedParamsMap.values()).filter(providedName => !specParamNamesLower.includes(providedName.toLowerCase()));
4041
+ if (missing.length > 0 || extra.length > 0) {
4042
+ let message = `Query '${queryName}' parameter mismatch. `;
4043
+ if (missing.length > 0) {
4044
+ message += `Missing: ${missing.join(', ')}. `;
4045
+ }
4046
+ if (extra.length > 0) {
4047
+ message += `Unknown: ${extra.join(', ')}. `;
4048
+ }
4049
+ // Generate correct parameters object
4050
+ const correctParams = specQuery.parameters
4051
+ .map(p => {
4052
+ // Check if we have this param (case-insensitive)
4053
+ const providedName = providedParamsMap.get(p.name.toLowerCase());
4054
+ if (providedName) {
4055
+ // Keep existing value, find the property with case-insensitive match
4056
+ const existingProp = paramValue.properties.find(prop => t.isObjectProperty(prop) &&
4057
+ t.isIdentifier(prop.key) &&
4058
+ prop.key.name.toLowerCase() === p.name.toLowerCase());
4059
+ if (existingProp && t.isStringLiteral(existingProp.value)) {
4060
+ return ` ${p.name}: '${existingProp.value.value}'`;
4061
+ }
4062
+ else if (existingProp && t.isNumericLiteral(existingProp.value)) {
4063
+ return ` ${p.name}: ${existingProp.value.value}`;
4064
+ }
4065
+ else if (existingProp && t.isIdentifier(existingProp.value)) {
4066
+ return ` ${p.name}: ${existingProp.value.name}`;
4067
+ }
4068
+ }
4069
+ // Add missing with test value
4070
+ return ` ${p.name}: '${p.testValue || 'value'}'`;
4071
+ })
4072
+ .join(',\n');
4073
+ violations.push({
4074
+ rule: 'runquery-parameters-validation',
4075
+ severity: 'high',
4076
+ line: parametersNode.loc?.start.line || 0,
4077
+ column: parametersNode.loc?.start.column || 0,
4078
+ message: message + `Expected: {${specParamNames.join(', ')}}`,
4079
+ code: `Parameters: {\n${correctParams}\n}`
4080
+ });
4081
+ }
4082
+ }
4083
+ // Case 3: Parameters is neither array nor object
4084
+ else if (!t.isObjectExpression(paramValue)) {
4085
+ let fixCode;
4086
+ let message;
4087
+ if (specQuery?.parameters && specQuery.parameters.length > 0) {
4088
+ const exampleParams = specQuery.parameters
4089
+ .map(p => ` ${p.name}: '${p.testValue || 'value'}'`)
4090
+ .join(',\n');
4091
+ fixCode = `Parameters: {\n${exampleParams}\n}`;
4092
+ message = `RunQuery Parameters must be object. Expected params from spec: ${specQuery.parameters.map(p => p.name).join(', ')}`;
4093
+ }
4094
+ else {
4095
+ fixCode = `Parameters: {\n paramName: 'value'\n}`;
4096
+ message = `RunQuery Parameters must be object with key-value pairs`;
4097
+ }
4098
+ violations.push({
4099
+ rule: 'runquery-parameters-validation',
4100
+ severity: 'critical',
4101
+ line: parametersNode.loc?.start.line || 0,
4102
+ column: parametersNode.loc?.start.column || 0,
4103
+ message,
4104
+ code: fixCode
4105
+ });
4106
+ }
4107
+ // Additional check: Validate against spec queries list
4108
+ if (queryName && componentSpec?.dataRequirements?.queries) {
4109
+ const queryExists = componentSpec.dataRequirements.queries.some(q => q.name === queryName);
4110
+ if (!queryExists) {
4111
+ const availableQueries = componentSpec.dataRequirements.queries.map(q => q.name).join(', ');
4112
+ violations.push({
4113
+ rule: 'runquery-parameters-validation',
4114
+ severity: 'high',
4115
+ line: path.node.loc?.start.line || 0,
4116
+ column: path.node.loc?.start.column || 0,
4117
+ message: `Query '${queryName}' not found in component spec. Available queries: ${availableQueries || 'none'}`,
4118
+ code: `QueryName: '${componentSpec.dataRequirements.queries[0]?.name || 'QueryNameFromSpec'}'`
4119
+ });
4120
+ }
4121
+ }
4122
+ }
4123
+ }
4124
+ });
4125
+ return violations;
4126
+ }
4127
+ },
4128
+ {
4129
+ name: 'runview-entity-validation',
4130
+ appliesTo: 'all',
4131
+ test: (ast, componentName, componentSpec) => {
4132
+ const violations = [];
4133
+ (0, traverse_1.default)(ast, {
4134
+ CallExpression(path) {
4135
+ const callee = path.node.callee;
4136
+ // Check for utilities.rv.RunView or RunViews
4137
+ if (t.isMemberExpression(callee) &&
4138
+ t.isMemberExpression(callee.object) &&
4139
+ t.isIdentifier(callee.object.object) &&
4140
+ callee.object.object.name === 'utilities' &&
4141
+ t.isIdentifier(callee.object.property) &&
4142
+ callee.object.property.name === 'rv' &&
4143
+ t.isIdentifier(callee.property)) {
4144
+ const methodName = callee.property.name;
4145
+ if (methodName !== 'RunView' && methodName !== 'RunViews')
4146
+ return;
4147
+ // Get the configs
4148
+ let configs = [];
4149
+ if (methodName === 'RunViews' && t.isArrayExpression(path.node.arguments[0])) {
4150
+ configs = path.node.arguments[0].elements
4151
+ .filter((e) => t.isObjectExpression(e));
4152
+ }
4153
+ else if (methodName === 'RunView' && t.isObjectExpression(path.node.arguments[0])) {
4154
+ configs = [path.node.arguments[0]];
4155
+ }
4156
+ // Check each config against spec
4157
+ if (componentSpec?.dataRequirements?.entities) {
4158
+ const specEntityNames = componentSpec.dataRequirements.entities.map(e => e.name);
4159
+ for (const config of configs) {
4160
+ let entityName = null;
4161
+ for (const prop of config.properties) {
4162
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
4163
+ if (prop.key.name === 'EntityName' && t.isStringLiteral(prop.value)) {
4164
+ entityName = prop.value.value;
4165
+ break;
4166
+ }
4167
+ }
4168
+ }
4169
+ if (entityName && specEntityNames.length > 0 && !specEntityNames.includes(entityName)) {
4170
+ violations.push({
4171
+ rule: 'runview-entity-validation',
4172
+ severity: 'medium',
4173
+ line: config.loc?.start.line || 0,
4174
+ column: config.loc?.start.column || 0,
4175
+ message: `Entity '${entityName}' not in component spec. Available entities: ${specEntityNames.join(', ')}`,
4176
+ code: `EntityName: '${specEntityNames[0] || 'EntityFromSpec'}'`
4177
+ });
4178
+ }
4179
+ }
4180
+ }
4181
+ }
4182
+ }
4183
+ });
4184
+ return violations;
4185
+ }
4186
+ },
4187
+ {
4188
+ name: 'react-component-naming',
4189
+ appliesTo: 'all',
4190
+ test: (ast, componentName, componentSpec) => {
4191
+ const violations = [];
4192
+ (0, traverse_1.default)(ast, {
4193
+ FunctionDeclaration(path) {
4194
+ if (path.node.id && path.node.id.name === componentName) {
4195
+ // Check if it's the main component function
4196
+ const funcName = path.node.id.name;
4197
+ // Check if function has component-like parameters (props structure)
4198
+ const firstParam = path.node.params[0];
4199
+ const hasComponentProps = firstParam && (t.isObjectPattern(firstParam) ||
4200
+ t.isIdentifier(firstParam));
4201
+ if (hasComponentProps && funcName[0] !== funcName[0].toUpperCase()) {
4202
+ violations.push({
4203
+ rule: 'react-component-naming',
4204
+ severity: 'critical',
4205
+ line: path.node.id.loc?.start.line || 0,
4206
+ column: path.node.id.loc?.start.column || 0,
4207
+ message: `React component "${funcName}" must start with uppercase. JSX treats lowercase as HTML elements.`,
4208
+ code: `function ${funcName[0].toUpperCase()}${funcName.slice(1)}`
4209
+ });
4210
+ }
4211
+ }
4212
+ // Also check for any other component-like functions
4213
+ if (path.node.id && path.node.params[0]) {
4214
+ const funcName = path.node.id.name;
4215
+ const firstParam = path.node.params[0];
4216
+ // Check if it looks like a component (has props parameter and returns JSX)
4217
+ let returnsJSX = false;
4218
+ path.traverse({
4219
+ ReturnStatement(returnPath) {
4220
+ if (returnPath.node.argument && t.isJSXElement(returnPath.node.argument)) {
4221
+ returnsJSX = true;
4222
+ }
4223
+ }
4224
+ });
4225
+ if (returnsJSX && t.isObjectPattern(firstParam)) {
4226
+ // Check if any props match component prop pattern
4227
+ const propNames = firstParam.properties
4228
+ .filter((p) => t.isObjectProperty(p))
4229
+ .filter(p => t.isIdentifier(p.key))
4230
+ .map(p => p.key.name);
4231
+ const hasComponentLikeProps = propNames.some(name => ['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings',
4232
+ 'data', 'userState', 'onStateChanged'].includes(name));
4233
+ if (hasComponentLikeProps && funcName[0] !== funcName[0].toUpperCase()) {
4234
+ violations.push({
4235
+ rule: 'react-component-naming',
4236
+ severity: 'critical',
4237
+ line: path.node.id.loc?.start.line || 0,
4238
+ column: path.node.id.loc?.start.column || 0,
4239
+ message: `Function "${funcName}" appears to be a React component and must start with uppercase.`,
4240
+ code: `function ${funcName[0].toUpperCase()}${funcName.slice(1)}`
4241
+ });
4242
+ }
4243
+ }
4244
+ }
4245
+ }
4246
+ });
4247
+ return violations;
4248
+ }
4249
+ },
4250
+ {
4251
+ name: 'string-template-validation',
4252
+ appliesTo: 'all',
4253
+ test: (ast, componentName, componentSpec) => {
4254
+ const violations = [];
4255
+ (0, traverse_1.default)(ast, {
4256
+ // Check for malformed template literals
4257
+ TemplateLiteral(path) {
4258
+ // Template literals are parsed correctly by Babel, so if we're here it's valid
4259
+ // But we can check for common issues like empty expressions
4260
+ path.node.expressions.forEach((expr, index) => {
4261
+ if (t.isIdentifier(expr) && expr.name === 'undefined') {
4262
+ violations.push({
4263
+ rule: 'string-template-validation',
4264
+ severity: 'high',
4265
+ line: expr.loc?.start.line || 0,
4266
+ column: expr.loc?.start.column || 0,
4267
+ message: 'Template literal contains undefined expression',
4268
+ code: '${/* value */}'
4269
+ });
4270
+ }
4271
+ });
4272
+ },
4273
+ // Check for string concatenation issues
4274
+ BinaryExpression(path) {
4275
+ if (path.node.operator === '+') {
4276
+ const left = path.node.left;
4277
+ const right = path.node.right;
4278
+ // Check for incomplete string concatenation patterns
4279
+ // e.g., 'text' + without right side, or + 'text' without left
4280
+ if (t.isStringLiteral(left) && t.isIdentifier(right) && right.name === 'undefined') {
4281
+ violations.push({
4282
+ rule: 'string-template-validation',
4283
+ severity: 'critical',
4284
+ line: path.node.loc?.start.line || 0,
4285
+ column: path.node.loc?.start.column || 0,
4286
+ message: 'String concatenation with undefined',
4287
+ code: `'${left.value}'`
4288
+ });
4289
+ }
4290
+ }
4291
+ },
4292
+ // Check for malformed return statements with strings
4293
+ ReturnStatement(path) {
4294
+ const arg = path.node.argument;
4295
+ // Look for patterns like: return ' + value (missing opening quote)
4296
+ if (t.isBinaryExpression(arg) && arg.operator === '+') {
4297
+ const left = arg.left;
4298
+ // Check if it starts with just a quote (malformed)
4299
+ if (t.isStringLiteral(left) && left.value === '') {
4300
+ const code = path.toString();
4301
+ // Check for patterns that suggest missing quotes
4302
+ if (code.includes("' +") || code.includes('" +')) {
4303
+ violations.push({
4304
+ rule: 'string-template-validation',
4305
+ severity: 'critical',
4306
+ line: path.node.loc?.start.line || 0,
4307
+ column: path.node.loc?.start.column || 0,
4308
+ message: 'Malformed string concatenation - possible missing quote',
4309
+ code: 'Check string quotes and concatenation'
4310
+ });
4311
+ }
4312
+ }
4313
+ }
4314
+ // Detect pattern like: return ' + y.toFixed(4)
4315
+ // This is checking for a literal string that starts with space and plus
4316
+ if (t.isCallExpression(arg)) {
4317
+ const code = path.toString();
4318
+ if (code.match(/return\s+['"`]\s*\+/)) {
4319
+ violations.push({
4320
+ rule: 'string-template-validation',
4321
+ severity: 'critical',
4322
+ line: path.node.loc?.start.line || 0,
4323
+ column: path.node.loc?.start.column || 0,
4324
+ message: 'Malformed string template - missing opening quote or backtick',
4325
+ code: `return \`$\{value}\``
4326
+ });
4327
+ }
4328
+ }
4329
+ },
4330
+ // Check inside function bodies for malformed strings
4331
+ StringLiteral(path) {
4332
+ const value = path.node.value;
4333
+ // Check for strings that look like incomplete templates
4334
+ // e.g., a string that starts with $ but isn't in a template
4335
+ // Check if this string literal is not inside a template literal
4336
+ let isInTemplate = false;
4337
+ let currentPath = path.parentPath;
4338
+ while (currentPath) {
4339
+ if (t.isTemplateLiteral(currentPath.node)) {
4340
+ isInTemplate = true;
4341
+ break;
4342
+ }
4343
+ currentPath = currentPath.parentPath;
4344
+ }
4345
+ if (value.includes('${') && !isInTemplate) {
4346
+ violations.push({
4347
+ rule: 'string-template-validation',
4348
+ severity: 'high',
4349
+ line: path.node.loc?.start.line || 0,
4350
+ column: path.node.loc?.start.column || 0,
4351
+ message: 'String contains template syntax but is not a template literal',
4352
+ code: `\`${value}\``
4353
+ });
4354
+ }
4355
+ }
4356
+ });
4357
+ // Additional check for specific malformed patterns in raw code
4358
+ const code = ast.toString ? ast.toString() : '';
4359
+ const lines = code.split('\n');
4360
+ lines.forEach((line, index) => {
4361
+ // Pattern: return ' + something or return " + something
4362
+ const malformedReturn = line.match(/return\s+['"`]\s*\+\s*[\w.()]/);
4363
+ if (malformedReturn) {
4364
+ violations.push({
4365
+ rule: 'string-template-validation',
4366
+ severity: 'critical',
4367
+ line: index + 1,
4368
+ column: malformedReturn.index || 0,
4369
+ message: 'Malformed string return - missing opening quote',
4370
+ code: 'return `${value}`'
4371
+ });
4372
+ }
4373
+ // Pattern: unclosed template literal
4374
+ const templateStart = line.match(/`[^`]*\$\{[^}]*$/);
4375
+ if (templateStart && !line.includes('`', templateStart.index + 1)) {
4376
+ violations.push({
4377
+ rule: 'string-template-validation',
4378
+ severity: 'critical',
4379
+ line: index + 1,
4380
+ column: templateStart.index || 0,
4381
+ message: 'Unclosed template literal',
4382
+ code: 'Close template with backtick'
4383
+ });
4384
+ }
4385
+ });
4386
+ return violations;
4387
+ }
4388
+ },
4389
+ {
4390
+ name: 'component-props-validation',
4391
+ appliesTo: 'all',
4392
+ test: (ast, componentName, componentSpec) => {
4393
+ const violations = [];
4394
+ const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
4395
+ // Build set of allowed props: standard props + componentSpec properties
4396
+ const allowedProps = new Set(standardProps);
4397
+ // Add props from componentSpec.properties if they exist
4398
+ if (componentSpec?.properties) {
4399
+ for (const prop of componentSpec.properties) {
4400
+ if (prop.name) {
4401
+ allowedProps.add(prop.name);
4402
+ }
4403
+ }
4404
+ }
4405
+ (0, traverse_1.default)(ast, {
4406
+ FunctionDeclaration(path) {
4407
+ if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
4408
+ const param = path.node.params[0];
4409
+ if (t.isObjectPattern(param)) {
4410
+ const invalidProps = [];
4411
+ const allProps = [];
4412
+ for (const prop of param.properties) {
4413
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
4414
+ const propName = prop.key.name;
4415
+ allProps.push(propName);
4416
+ if (!allowedProps.has(propName)) {
4417
+ invalidProps.push(propName);
4418
+ }
4419
+ }
4420
+ }
4421
+ // Only report if there are non-allowed props
4422
+ if (invalidProps.length > 0) {
4423
+ const customPropsMessage = componentSpec?.properties?.length
4424
+ ? ` and custom props defined in spec: ${componentSpec.properties.map(p => p.name).join(', ')}`
4425
+ : '';
4426
+ violations.push({
4427
+ rule: 'component-props-validation',
4428
+ severity: 'critical',
4429
+ line: path.node.loc?.start.line || 0,
4430
+ column: path.node.loc?.start.column || 0,
4431
+ message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
4432
+ });
4433
+ }
4434
+ }
4435
+ }
4436
+ },
4437
+ // Also check arrow function components
4438
+ VariableDeclarator(path) {
4439
+ if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
4440
+ const init = path.node.init;
4441
+ if (t.isArrowFunctionExpression(init) && init.params[0]) {
4442
+ const param = init.params[0];
4443
+ if (t.isObjectPattern(param)) {
4444
+ const invalidProps = [];
4445
+ const allProps = [];
4446
+ for (const prop of param.properties) {
4447
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
4448
+ const propName = prop.key.name;
4449
+ allProps.push(propName);
4450
+ if (!allowedProps.has(propName)) {
4451
+ invalidProps.push(propName);
4452
+ }
4453
+ }
4454
+ }
4455
+ if (invalidProps.length > 0) {
4456
+ const customPropsMessage = componentSpec?.properties?.length
4457
+ ? ` and custom props defined in spec: ${componentSpec.properties.map(p => p.name).join(', ')}`
4458
+ : '';
4459
+ violations.push({
4460
+ rule: 'component-props-validation',
3105
4461
  severity: 'critical',
3106
4462
  line: path.node.loc?.start.line || 0,
3107
4463
  column: path.node.loc?.start.column || 0,
3108
- message: `Component "${componentName}" accepts non-standard props: ${invalidProps.join(', ')}. Root components can only accept standard props: ${Array.from(standardProps).join(', ')}. Load data internally using utilities.rv.RunView().`
4464
+ message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
3109
4465
  });
3110
4466
  }
3111
4467
  }
@@ -3144,7 +4500,7 @@ ComponentLinter.universalComponentRules = [
3144
4500
  }
3145
4501
  }
3146
4502
  }
3147
- // Check for invalid destructuring from components
4503
+ // Check for manual destructuring from components (now optional since auto-destructuring is in place)
3148
4504
  (0, traverse_1.default)(ast, {
3149
4505
  VariableDeclarator(path) {
3150
4506
  // Look for: const { Something } = components;
@@ -3178,6 +4534,17 @@ ComponentLinter.universalComponentRules = [
3178
4534
  });
3179
4535
  }
3180
4536
  }
4537
+ else {
4538
+ // Valid component, but manual destructuring is now redundant
4539
+ violations.push({
4540
+ rule: 'invalid-components-destructuring',
4541
+ severity: 'low',
4542
+ line: prop.loc?.start.line || 0,
4543
+ column: prop.loc?.start.column || 0,
4544
+ message: `Manual destructuring of "${destructuredName}" from components prop is redundant. Components are now auto-destructured and available directly.`,
4545
+ code: `const { ${destructuredName} } = components; // Can be removed`
4546
+ });
4547
+ }
3181
4548
  }
3182
4549
  }
3183
4550
  }
@@ -3223,10 +4590,26 @@ ComponentLinter.universalComponentRules = [
3223
4590
  }
3224
4591
  }
3225
4592
  },
3226
- // Check for unsafe array operations
4593
+ // Check for direct array access patterns like arr[0]
3227
4594
  MemberExpression(path) {
3228
- const { object, property } = path.node;
3229
- // Check for array methods that will crash on undefined
4595
+ const { object, property, computed } = path.node;
4596
+ // Check for array[index] patterns
4597
+ if (computed && (t.isNumericLiteral(property) ||
4598
+ (t.isIdentifier(property) && /^\d+$/.test(property.name)))) {
4599
+ const code = path.toString();
4600
+ // Check if there's optional chaining
4601
+ if (!path.node.optional) {
4602
+ violations.push({
4603
+ rule: 'unsafe-array-operations',
4604
+ severity: 'low',
4605
+ line: path.node.loc?.start.line || 0,
4606
+ column: path.node.loc?.start.column || 0,
4607
+ message: `Direct array access "${code}" may be undefined. Consider using optional chaining: ${code.replace('[', '?.[')} or check array bounds first.`,
4608
+ code: code.substring(0, 50)
4609
+ });
4610
+ }
4611
+ }
4612
+ // Check for array methods that could fail on undefined
3230
4613
  const unsafeArrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'length'];
3231
4614
  if (t.isIdentifier(property) && unsafeArrayMethods.includes(property.name)) {
3232
4615
  // Check if the object is a prop parameter
@@ -3317,250 +4700,422 @@ ComponentLinter.universalComponentRules = [
3317
4700
  }
3318
4701
  if (!hasGuard) {
3319
4702
  const methodName = property.name;
3320
- const severity = methodName === 'length' ? 'high' : 'high';
3321
4703
  violations.push({
3322
4704
  rule: 'unsafe-array-operations',
3323
- severity,
4705
+ severity: 'low',
3324
4706
  line: path.node.loc?.start.line || 0,
3325
4707
  column: path.node.loc?.start.column || 0,
3326
- message: `Unsafe operation "${object.name}.${methodName}" on prop that may be undefined. Props from queries/RunView can be null/undefined on initial render. Add a guard: if (!${object.name} || !Array.isArray(${object.name})) return <div>Loading...</div>; OR use: ${object.name}?.${methodName} or (${object.name} || []).${methodName}`,
4708
+ message: `Potentially unsafe operation "${object.name}.${methodName}" on prop that may be undefined. Consider using optional chaining: ${object.name}?.${methodName} or provide a default: (${object.name} || []).${methodName}`,
3327
4709
  code: `${object.name}.${methodName}`
3328
4710
  });
3329
4711
  }
3330
4712
  }
3331
4713
  }
3332
4714
  }
4715
+ },
4716
+ // Check for reduce without initial value
4717
+ CallExpression(path) {
4718
+ if (t.isMemberExpression(path.node.callee) &&
4719
+ t.isIdentifier(path.node.callee.property) &&
4720
+ path.node.callee.property.name === 'reduce') {
4721
+ const hasInitialValue = path.node.arguments.length > 1;
4722
+ if (!hasInitialValue) {
4723
+ const code = path.toString();
4724
+ violations.push({
4725
+ rule: 'unsafe-array-operations',
4726
+ severity: 'low',
4727
+ line: path.node.loc?.start.line || 0,
4728
+ column: path.node.loc?.start.column || 0,
4729
+ message: `reduce() without initial value may fail on empty arrays. Consider providing an initial value as the second argument.`,
4730
+ code: code.substring(0, 100)
4731
+ });
4732
+ }
4733
+ }
3333
4734
  }
3334
4735
  });
3335
4736
  return violations;
3336
4737
  }
3337
4738
  },
3338
- {
3339
- name: 'undefined-jsx-component',
3340
- appliesTo: 'all',
3341
- test: (ast, componentName, componentSpec) => {
3342
- const violations = [];
3343
- // Track what's available in scope
3344
- const availableIdentifiers = new Set();
3345
- const componentsFromProp = new Set();
3346
- const libraryGlobalVars = new Set();
3347
- // Add React hooks and built-ins
3348
- availableIdentifiers.add('React');
3349
- REACT_BUILT_INS.forEach(name => availableIdentifiers.add(name));
3350
- availableIdentifiers.add('useState');
3351
- availableIdentifiers.add('useEffect');
3352
- availableIdentifiers.add('useCallback');
3353
- availableIdentifiers.add('useMemo');
3354
- availableIdentifiers.add('useRef');
3355
- availableIdentifiers.add('useContext');
3356
- availableIdentifiers.add('useReducer');
3357
- availableIdentifiers.add('useLayoutEffect');
3358
- // Add HTML elements from our comprehensive list
3359
- HTML_ELEMENTS.forEach(el => availableIdentifiers.add(el));
3360
- // Add library global variables
3361
- if (componentSpec?.libraries) {
3362
- for (const lib of componentSpec.libraries) {
3363
- if (lib.globalVariable) {
3364
- libraryGlobalVars.add(lib.globalVariable);
3365
- availableIdentifiers.add(lib.globalVariable);
3366
- }
4739
+ // DISABLED: Too many false positives - needs better dependency/library checking
4740
+ // Re-enable after improving to check dependencies before assuming library components
4741
+ /* {
4742
+ name: 'undefined-jsx-component',
4743
+ appliesTo: 'all',
4744
+ test: (ast: t.File, componentName: string, componentSpec?: ComponentSpec) => {
4745
+ const violations: Violation[] = [];
4746
+
4747
+ // Track what's available in scope
4748
+ const availableIdentifiers = new Set<string>();
4749
+ const componentsFromProp = new Set<string>();
4750
+ const libraryGlobalVars = new Set<string>();
4751
+
4752
+ // Add React hooks and built-ins
4753
+ availableIdentifiers.add('React');
4754
+ REACT_BUILT_INS.forEach(name => availableIdentifiers.add(name));
4755
+ availableIdentifiers.add('useState');
4756
+ availableIdentifiers.add('useEffect');
4757
+ availableIdentifiers.add('useCallback');
4758
+ availableIdentifiers.add('useMemo');
4759
+ availableIdentifiers.add('useRef');
4760
+ availableIdentifiers.add('useContext');
4761
+ availableIdentifiers.add('useReducer');
4762
+ availableIdentifiers.add('useLayoutEffect');
4763
+
4764
+ // Add HTML elements from our comprehensive list
4765
+ HTML_ELEMENTS.forEach(el => availableIdentifiers.add(el));
4766
+
4767
+ // Add library global variables
4768
+ if (componentSpec?.libraries) {
4769
+ for (const lib of componentSpec.libraries) {
4770
+ if (lib.globalVariable) {
4771
+ libraryGlobalVars.add(lib.globalVariable);
4772
+ availableIdentifiers.add(lib.globalVariable);
4773
+ }
4774
+ }
4775
+ }
4776
+
4777
+ // Track dependency components (these are now auto-destructured in the wrapper)
4778
+ if (componentSpec?.dependencies) {
4779
+ for (const dep of componentSpec.dependencies) {
4780
+ if (dep.name) {
4781
+ componentsFromProp.add(dep.name);
4782
+ // Mark as available since they're auto-destructured
4783
+ availableIdentifiers.add(dep.name);
4784
+ }
4785
+ }
4786
+ }
4787
+
4788
+ traverse(ast, {
4789
+ // Track variable declarations
4790
+ VariableDeclarator(path: NodePath<t.VariableDeclarator>) {
4791
+ if (t.isIdentifier(path.node.id)) {
4792
+ availableIdentifiers.add(path.node.id.name);
4793
+ } else if (t.isObjectPattern(path.node.id)) {
4794
+ // Track destructured variables
4795
+ for (const prop of path.node.id.properties) {
4796
+ if (t.isObjectProperty(prop)) {
4797
+ if (t.isIdentifier(prop.value)) {
4798
+ availableIdentifiers.add(prop.value.name);
4799
+ } else if (t.isIdentifier(prop.key)) {
4800
+ availableIdentifiers.add(prop.key.name);
4801
+ }
3367
4802
  }
4803
+ }
3368
4804
  }
3369
- // Track what's destructured from components
3370
- if (componentSpec?.dependencies) {
3371
- for (const dep of componentSpec.dependencies) {
3372
- if (dep.name) {
3373
- componentsFromProp.add(dep.name);
4805
+ },
4806
+
4807
+ // Track function declarations
4808
+ FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {
4809
+ if (path.node.id) {
4810
+ availableIdentifiers.add(path.node.id.name);
4811
+ }
4812
+ },
4813
+
4814
+ // Check JSX elements
4815
+ JSXElement(path: NodePath<t.JSXElement>) {
4816
+ const openingElement = path.node.openingElement;
4817
+
4818
+ // Handle JSXMemberExpression (e.g., <library.Component>)
4819
+ if (t.isJSXMemberExpression(openingElement.name)) {
4820
+ let objectName = '';
4821
+
4822
+ if (t.isJSXIdentifier(openingElement.name.object)) {
4823
+ objectName = openingElement.name.object.name;
4824
+ }
4825
+
4826
+ // Check if the object (library global) is available
4827
+ if (objectName && !availableIdentifiers.has(objectName)) {
4828
+ // Check if it looks like a library global that should exist
4829
+ const isLikelyLibrary = /^[a-z][a-zA-Z]*$/.test(objectName) || // camelCase like agGrid
4830
+ /^[A-Z][a-zA-Z]*$/.test(objectName); // PascalCase like MaterialUI
4831
+
4832
+ if (isLikelyLibrary) {
4833
+ // Suggest available library globals
4834
+ const availableLibraries = Array.from(libraryGlobalVars);
4835
+
4836
+ if (availableLibraries.length > 0) {
4837
+ // Try to find a close match
4838
+ let suggestion = '';
4839
+ for (const lib of availableLibraries) {
4840
+ // Check for common patterns like agGridReact -> agGrid
4841
+ if (objectName.toLowerCase().includes(lib.toLowerCase().replace('grid', '')) ||
4842
+ lib.toLowerCase().includes(objectName.toLowerCase().replace('react', ''))) {
4843
+ suggestion = lib;
4844
+ break;
4845
+ }
3374
4846
  }
4847
+
4848
+ if (suggestion) {
4849
+ violations.push({
4850
+ rule: 'undefined-jsx-component',
4851
+ severity: 'critical',
4852
+ line: openingElement.loc?.start.line || 0,
4853
+ column: openingElement.loc?.start.column || 0,
4854
+ message: `Library global "${objectName}" is not defined. Did you mean "${suggestion}"? Available library globals: ${availableLibraries.join(', ')}`,
4855
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
4856
+ });
4857
+ } else {
4858
+ violations.push({
4859
+ rule: 'undefined-jsx-component',
4860
+ severity: 'critical',
4861
+ line: openingElement.loc?.start.line || 0,
4862
+ column: openingElement.loc?.start.column || 0,
4863
+ message: `Library global "${objectName}" is not defined. Available library globals: ${availableLibraries.join(', ')}`,
4864
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
4865
+ });
4866
+ }
4867
+ } else {
4868
+ violations.push({
4869
+ rule: 'undefined-jsx-component',
4870
+ severity: 'critical',
4871
+ line: openingElement.loc?.start.line || 0,
4872
+ column: openingElement.loc?.start.column || 0,
4873
+ message: `"${objectName}" is not defined. It appears to be a library global, but no libraries are specified in the component specification.`,
4874
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
4875
+ });
4876
+ }
4877
+ } else {
4878
+ // Not a typical library pattern, just undefined
4879
+ violations.push({
4880
+ rule: 'undefined-jsx-component',
4881
+ severity: 'critical',
4882
+ line: openingElement.loc?.start.line || 0,
4883
+ column: openingElement.loc?.start.column || 0,
4884
+ message: `"${objectName}" is not defined in the current scope.`,
4885
+ code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
4886
+ });
3375
4887
  }
4888
+ }
4889
+ return; // Done with member expression
3376
4890
  }
3377
- (0, traverse_1.default)(ast, {
3378
- // Track variable declarations
3379
- VariableDeclarator(path) {
3380
- if (t.isIdentifier(path.node.id)) {
3381
- availableIdentifiers.add(path.node.id.name);
4891
+
4892
+ // Handle regular JSXIdentifier (e.g., <Component>)
4893
+ if (t.isJSXIdentifier(openingElement.name)) {
4894
+ const tagName = openingElement.name.name;
4895
+
4896
+ // Check if this component is available in scope
4897
+ if (!availableIdentifiers.has(tagName)) {
4898
+ // It's not defined - check if it's a built-in or needs to be defined
4899
+ const isHTMLElement = HTML_ELEMENTS.has(tagName.toLowerCase());
4900
+ const isReactBuiltIn = REACT_BUILT_INS.has(tagName);
4901
+
4902
+ if (!isHTMLElement && !isReactBuiltIn) {
4903
+ // Not a built-in element, so it needs to be defined
4904
+ // Check if it looks like PascalCase (likely a component)
4905
+ const isPascalCase = /^[A-Z][a-zA-Z0-9]*$/.test(tagName);
4906
+
4907
+ if (isPascalCase) {
4908
+ // Check what libraries are actually available in the spec
4909
+ const availableLibraries = componentSpec?.libraries || [];
4910
+
4911
+ if (availableLibraries.length > 0) {
4912
+ // We have libraries available - provide specific guidance
4913
+ const libraryNames = availableLibraries
4914
+ .filter(lib => lib.globalVariable)
4915
+ .map(lib => lib.globalVariable);
4916
+
4917
+ if (libraryNames.length === 1) {
4918
+ // Single library - be very specific
4919
+ violations.push({
4920
+ rule: 'undefined-jsx-component',
4921
+ severity: 'critical',
4922
+ line: openingElement.loc?.start.line || 0,
4923
+ column: openingElement.loc?.start.column || 0,
4924
+ 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.`,
4925
+ code: `<${tagName} ... />`
4926
+ });
4927
+ } else {
4928
+ // Multiple libraries - suggest checking which one
4929
+ violations.push({
4930
+ rule: 'undefined-jsx-component',
4931
+ severity: 'critical',
4932
+ line: openingElement.loc?.start.line || 0,
4933
+ column: openingElement.loc?.start.column || 0,
4934
+ message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}. Destructure it from the appropriate library, e.g., const { ${tagName} } = LibraryName;`,
4935
+ code: `<${tagName} ... />`
4936
+ });
3382
4937
  }
3383
- else if (t.isObjectPattern(path.node.id)) {
3384
- // Track destructured variables
3385
- for (const prop of path.node.id.properties) {
3386
- if (t.isObjectProperty(prop)) {
3387
- if (t.isIdentifier(prop.value)) {
3388
- availableIdentifiers.add(prop.value.name);
3389
- }
3390
- else if (t.isIdentifier(prop.key)) {
3391
- availableIdentifiers.add(prop.key.name);
3392
- }
3393
- }
4938
+ } else {
4939
+ // No libraries in spec but looks like a library component
4940
+ violations.push({
4941
+ rule: 'undefined-jsx-component',
4942
+ severity: 'critical',
4943
+ line: openingElement.loc?.start.line || 0,
4944
+ column: openingElement.loc?.start.column || 0,
4945
+ message: `JSX component "${tagName}" is not defined. This appears to be a library component, but no libraries have been specified in the component specification. The use of external libraries has not been authorized for this component. Components without library specifications cannot use external libraries.`,
4946
+ code: `<${tagName} ... />`
4947
+ });
4948
+ }
4949
+ } else if (componentsFromProp.has(tagName)) {
4950
+ // This shouldn't happen since dependency components are auto-destructured
4951
+ // But keep as a fallback check
4952
+ violations.push({
4953
+ rule: 'undefined-jsx-component',
4954
+ severity: 'high',
4955
+ line: openingElement.loc?.start.line || 0,
4956
+ column: openingElement.loc?.start.column || 0,
4957
+ message: `JSX component "${tagName}" is in dependencies but appears to be undefined. There may be an issue with component registration.`,
4958
+ code: `<${tagName} ... />`
4959
+ });
4960
+ } else {
4961
+ // Unknown component - not in libraries, not in dependencies
4962
+ violations.push({
4963
+ rule: 'undefined-jsx-component',
4964
+ severity: 'high',
4965
+ line: openingElement.loc?.start.line || 0,
4966
+ column: openingElement.loc?.start.column || 0,
4967
+ 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.`,
4968
+ code: `<${tagName} ... />`
4969
+ });
4970
+ }
4971
+ } else {
4972
+ // Not PascalCase but also not a built-in - suspicious
4973
+ violations.push({
4974
+ rule: 'undefined-jsx-component',
4975
+ severity: 'medium',
4976
+ line: openingElement.loc?.start.line || 0,
4977
+ column: openingElement.loc?.start.column || 0,
4978
+ message: `JSX element "${tagName}" is not recognized as a valid HTML element or React component. Check the spelling or ensure it's properly defined.`,
4979
+ code: `<${tagName} ... />`
4980
+ });
4981
+ }
4982
+ }
4983
+ }
4984
+ }
4985
+ });
4986
+
4987
+ return violations;
4988
+ }
4989
+ }, */
4990
+ {
4991
+ name: 'runquery-runview-validation',
4992
+ appliesTo: 'all',
4993
+ test: (ast, componentName, componentSpec) => {
4994
+ const violations = [];
4995
+ // Extract declared queries and entities from dataRequirements
4996
+ const declaredQueries = new Set();
4997
+ const declaredEntities = new Set();
4998
+ if (componentSpec?.dataRequirements) {
4999
+ // Handle queries in different possible locations
5000
+ if (Array.isArray(componentSpec.dataRequirements)) {
5001
+ // If it's an array directly
5002
+ componentSpec.dataRequirements.forEach((req) => {
5003
+ if (req.type === 'query' && req.name) {
5004
+ declaredQueries.add(req.name.toLowerCase());
5005
+ }
5006
+ if (req.type === 'entity' && req.name) {
5007
+ declaredEntities.add(req.name.toLowerCase());
3394
5008
  }
5009
+ });
5010
+ }
5011
+ else if (typeof componentSpec.dataRequirements === 'object') {
5012
+ // If it's an object with queries/entities properties
5013
+ if (componentSpec.dataRequirements.queries) {
5014
+ componentSpec.dataRequirements.queries.forEach((q) => {
5015
+ if (q.name)
5016
+ declaredQueries.add(q.name.toLowerCase());
5017
+ });
3395
5018
  }
3396
- },
3397
- // Track function declarations
3398
- FunctionDeclaration(path) {
3399
- if (path.node.id) {
3400
- availableIdentifiers.add(path.node.id.name);
5019
+ if (componentSpec.dataRequirements.entities) {
5020
+ componentSpec.dataRequirements.entities.forEach((e) => {
5021
+ if (e.name)
5022
+ declaredEntities.add(e.name.toLowerCase());
5023
+ });
3401
5024
  }
3402
- },
3403
- // Check JSX elements
3404
- JSXElement(path) {
3405
- const openingElement = path.node.openingElement;
3406
- // Handle JSXMemberExpression (e.g., <library.Component>)
3407
- if (t.isJSXMemberExpression(openingElement.name)) {
3408
- let objectName = '';
3409
- if (t.isJSXIdentifier(openingElement.name.object)) {
3410
- objectName = openingElement.name.object.name;
3411
- }
3412
- // Check if the object (library global) is available
3413
- if (objectName && !availableIdentifiers.has(objectName)) {
3414
- // Check if it looks like a library global that should exist
3415
- const isLikelyLibrary = /^[a-z][a-zA-Z]*$/.test(objectName) || // camelCase like agGrid
3416
- /^[A-Z][a-zA-Z]*$/.test(objectName); // PascalCase like MaterialUI
3417
- if (isLikelyLibrary) {
3418
- // Suggest available library globals
3419
- const availableLibraries = Array.from(libraryGlobalVars);
3420
- if (availableLibraries.length > 0) {
3421
- // Try to find a close match
3422
- let suggestion = '';
3423
- for (const lib of availableLibraries) {
3424
- // Check for common patterns like agGridReact -> agGrid
3425
- if (objectName.toLowerCase().includes(lib.toLowerCase().replace('grid', '')) ||
3426
- lib.toLowerCase().includes(objectName.toLowerCase().replace('react', ''))) {
3427
- suggestion = lib;
3428
- break;
3429
- }
3430
- }
3431
- if (suggestion) {
3432
- violations.push({
3433
- rule: 'undefined-jsx-component',
3434
- severity: 'critical',
3435
- line: openingElement.loc?.start.line || 0,
3436
- column: openingElement.loc?.start.column || 0,
3437
- message: `Library global "${objectName}" is not defined. Did you mean "${suggestion}"? Available library globals: ${availableLibraries.join(', ')}`,
3438
- code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
3439
- });
3440
- }
3441
- else {
5025
+ }
5026
+ }
5027
+ (0, traverse_1.default)(ast, {
5028
+ CallExpression(path) {
5029
+ const callee = path.node.callee;
5030
+ // Check for RunQuery calls
5031
+ if (t.isMemberExpression(callee) &&
5032
+ t.isIdentifier(callee.property) &&
5033
+ callee.property.name === 'RunQuery') {
5034
+ const args = path.node.arguments;
5035
+ if (args.length > 0 && t.isObjectExpression(args[0])) {
5036
+ const props = args[0].properties;
5037
+ // Find QueryName property
5038
+ const queryNameProp = props.find(p => t.isObjectProperty(p) &&
5039
+ t.isIdentifier(p.key) &&
5040
+ p.key.name === 'QueryName');
5041
+ if (queryNameProp && t.isObjectProperty(queryNameProp)) {
5042
+ const value = queryNameProp.value;
5043
+ // Check if it's a string literal
5044
+ if (t.isStringLiteral(value)) {
5045
+ const queryName = value.value;
5046
+ // Check if it looks like SQL (contains SELECT, FROM, etc.)
5047
+ const sqlKeywords = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'FROM', 'WHERE', 'JOIN'];
5048
+ const upperQuery = queryName.toUpperCase();
5049
+ const looksLikeSQL = sqlKeywords.some(keyword => upperQuery.includes(keyword));
5050
+ if (looksLikeSQL) {
3442
5051
  violations.push({
3443
- rule: 'undefined-jsx-component',
5052
+ rule: 'runquery-runview-validation',
3444
5053
  severity: 'critical',
3445
- line: openingElement.loc?.start.line || 0,
3446
- column: openingElement.loc?.start.column || 0,
3447
- message: `Library global "${objectName}" is not defined. Available library globals: ${availableLibraries.join(', ')}`,
3448
- code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
5054
+ line: value.loc?.start.line || 0,
5055
+ column: value.loc?.start.column || 0,
5056
+ message: `RunQuery cannot accept SQL statements. QueryName must be a registered query name, not SQL: "${queryName.substring(0, 50)}..."`,
5057
+ code: value.value.substring(0, 100)
3449
5058
  });
3450
5059
  }
3451
- }
3452
- else {
3453
- violations.push({
3454
- rule: 'undefined-jsx-component',
3455
- severity: 'critical',
3456
- line: openingElement.loc?.start.line || 0,
3457
- column: openingElement.loc?.start.column || 0,
3458
- message: `"${objectName}" is not defined. It appears to be a library global, but no libraries are specified in the component specification.`,
3459
- code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
3460
- });
3461
- }
3462
- }
3463
- else {
3464
- // Not a typical library pattern, just undefined
3465
- violations.push({
3466
- rule: 'undefined-jsx-component',
3467
- severity: 'critical',
3468
- line: openingElement.loc?.start.line || 0,
3469
- column: openingElement.loc?.start.column || 0,
3470
- message: `"${objectName}" is not defined in the current scope.`,
3471
- code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
3472
- });
3473
- }
3474
- }
3475
- return; // Done with member expression
3476
- }
3477
- // Handle regular JSXIdentifier (e.g., <Component>)
3478
- if (t.isJSXIdentifier(openingElement.name)) {
3479
- const tagName = openingElement.name.name;
3480
- // Check if this component is available in scope
3481
- if (!availableIdentifiers.has(tagName)) {
3482
- // It's not defined - check if it's a built-in or needs to be defined
3483
- const isHTMLElement = HTML_ELEMENTS.has(tagName.toLowerCase());
3484
- const isReactBuiltIn = REACT_BUILT_INS.has(tagName);
3485
- if (!isHTMLElement && !isReactBuiltIn) {
3486
- // Not a built-in element, so it needs to be defined
3487
- // Check if it looks like PascalCase (likely a component)
3488
- const isPascalCase = /^[A-Z][a-zA-Z0-9]*$/.test(tagName);
3489
- if (isPascalCase) {
3490
- // Check what libraries are actually available in the spec
3491
- const availableLibraries = componentSpec?.libraries || [];
3492
- if (availableLibraries.length > 0) {
3493
- // We have libraries available - provide specific guidance
3494
- const libraryNames = availableLibraries
3495
- .filter(lib => lib.globalVariable)
3496
- .map(lib => lib.globalVariable);
3497
- if (libraryNames.length === 1) {
3498
- // Single library - be very specific
3499
- violations.push({
3500
- rule: 'undefined-jsx-component',
3501
- severity: 'critical',
3502
- line: openingElement.loc?.start.line || 0,
3503
- column: openingElement.loc?.start.column || 0,
3504
- 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.`,
3505
- code: `<${tagName} ... />`
3506
- });
3507
- }
3508
- else {
3509
- // Multiple libraries - suggest checking which one
3510
- violations.push({
3511
- rule: 'undefined-jsx-component',
3512
- severity: 'critical',
3513
- line: openingElement.loc?.start.line || 0,
3514
- column: openingElement.loc?.start.column || 0,
3515
- message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}. Destructure it from the appropriate library, e.g., const { ${tagName} } = LibraryName;`,
3516
- code: `<${tagName} ... />`
3517
- });
3518
- }
3519
- }
3520
- else {
3521
- // No libraries in spec but looks like a library component
5060
+ else if (declaredQueries.size > 0 && !declaredQueries.has(queryName.toLowerCase())) {
5061
+ // Only validate if we have declared queries
3522
5062
  violations.push({
3523
- rule: 'undefined-jsx-component',
3524
- severity: 'critical',
3525
- line: openingElement.loc?.start.line || 0,
3526
- column: openingElement.loc?.start.column || 0,
3527
- message: `JSX component "${tagName}" is not defined. This appears to be a library component, but no libraries have been specified in the component specification. The use of external libraries has not been authorized for this component. Components without library specifications cannot use external libraries.`,
3528
- code: `<${tagName} ... />`
5063
+ rule: 'runquery-runview-validation',
5064
+ severity: 'high',
5065
+ line: value.loc?.start.line || 0,
5066
+ column: value.loc?.start.column || 0,
5067
+ message: `Query "${queryName}" is not declared in dataRequirements.queries. Available queries: ${Array.from(declaredQueries).join(', ')}`,
5068
+ code: path.toString().substring(0, 100)
3529
5069
  });
3530
5070
  }
3531
5071
  }
3532
- else if (componentsFromProp.has(tagName)) {
3533
- // It's a component from the components prop
5072
+ else if (t.isIdentifier(value) || t.isTemplateLiteral(value)) {
5073
+ // Dynamic query name - check if it might be SQL
3534
5074
  violations.push({
3535
- rule: 'undefined-jsx-component',
3536
- severity: 'high',
3537
- line: openingElement.loc?.start.line || 0,
3538
- column: openingElement.loc?.start.column || 0,
3539
- message: `JSX component "${tagName}" is in dependencies but not destructured from components prop. Add: const { ${tagName} } = components;`,
3540
- code: `<${tagName} ... />`
5075
+ rule: 'runquery-runview-validation',
5076
+ severity: 'medium',
5077
+ line: value.loc?.start.line || 0,
5078
+ column: value.loc?.start.column || 0,
5079
+ message: `Dynamic QueryName detected. Ensure this is a query name, not a SQL statement.`,
5080
+ code: path.toString().substring(0, 100)
3541
5081
  });
3542
5082
  }
3543
- else {
3544
- // Unknown component - not in libraries, not in dependencies
5083
+ }
5084
+ }
5085
+ }
5086
+ // Check for RunView calls
5087
+ if (t.isMemberExpression(callee) &&
5088
+ t.isIdentifier(callee.property) &&
5089
+ (callee.property.name === 'RunView' || callee.property.name === 'RunViews')) {
5090
+ const args = path.node.arguments;
5091
+ // Handle both single object and array of objects
5092
+ const checkEntityName = (objExpr) => {
5093
+ const entityNameProp = objExpr.properties.find(p => t.isObjectProperty(p) &&
5094
+ t.isIdentifier(p.key) &&
5095
+ p.key.name === 'EntityName');
5096
+ if (entityNameProp && t.isObjectProperty(entityNameProp) && t.isStringLiteral(entityNameProp.value)) {
5097
+ const entityName = entityNameProp.value.value;
5098
+ if (declaredEntities.size > 0 && !declaredEntities.has(entityName.toLowerCase())) {
3545
5099
  violations.push({
3546
- rule: 'undefined-jsx-component',
5100
+ rule: 'runquery-runview-validation',
3547
5101
  severity: 'high',
3548
- line: openingElement.loc?.start.line || 0,
3549
- column: openingElement.loc?.start.column || 0,
3550
- 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.`,
3551
- code: `<${tagName} ... />`
5102
+ line: entityNameProp.value.loc?.start.line || 0,
5103
+ column: entityNameProp.value.loc?.start.column || 0,
5104
+ message: `Entity "${entityName}" is not declared in dataRequirements.entities. Available entities: ${Array.from(declaredEntities).join(', ')}`,
5105
+ code: path.toString().substring(0, 100)
3552
5106
  });
3553
5107
  }
3554
5108
  }
3555
- else {
3556
- // Not PascalCase but also not a built-in - suspicious
3557
- violations.push({
3558
- rule: 'undefined-jsx-component',
3559
- severity: 'medium',
3560
- line: openingElement.loc?.start.line || 0,
3561
- column: openingElement.loc?.start.column || 0,
3562
- message: `JSX element "${tagName}" is not recognized as a valid HTML element or React component. Check the spelling or ensure it's properly defined.`,
3563
- code: `<${tagName} ... />`
5109
+ };
5110
+ if (args.length > 0) {
5111
+ if (t.isObjectExpression(args[0])) {
5112
+ checkEntityName(args[0]);
5113
+ }
5114
+ else if (t.isArrayExpression(args[0])) {
5115
+ args[0].elements.forEach(elem => {
5116
+ if (t.isObjectExpression(elem)) {
5117
+ checkEntityName(elem);
5118
+ }
3564
5119
  });
3565
5120
  }
3566
5121
  }
@@ -3706,65 +5261,129 @@ ComponentLinter.universalComponentRules = [
3706
5261
  appliesTo: 'all',
3707
5262
  test: (ast, componentName, componentSpec) => {
3708
5263
  const violations = [];
3709
- // Valid properties for RunQueryResult
5264
+ // Valid properties for RunQueryResult based on MJCore type definition
3710
5265
  const validRunQueryResultProps = new Set([
3711
- 'QueryID', 'QueryName', 'Success', 'Results', 'RowCount',
3712
- 'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
5266
+ 'QueryID', // string
5267
+ 'QueryName', // string
5268
+ 'Success', // boolean
5269
+ 'Results', // any[]
5270
+ 'RowCount', // number
5271
+ 'TotalRowCount', // number
5272
+ 'ExecutionTime', // number
5273
+ 'ErrorMessage', // string
5274
+ 'AppliedParameters', // Record<string, any> (optional)
5275
+ 'CacheHit', // boolean (optional)
5276
+ 'CacheKey', // string (optional)
5277
+ 'CacheTTLRemaining' // number (optional)
3713
5278
  ]);
3714
- // Valid properties for RunViewResult
5279
+ // Valid properties for RunViewResult based on MJCore type definition
3715
5280
  const validRunViewResultProps = new Set([
3716
- 'Success', 'Results', 'UserViewRunID', 'RowCount',
3717
- 'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
5281
+ 'Success', // boolean
5282
+ 'Results', // Array<T>
5283
+ 'UserViewRunID', // string (optional)
5284
+ 'RowCount', // number
5285
+ 'TotalRowCount', // number
5286
+ 'ExecutionTime', // number
5287
+ 'ErrorMessage' // string
3718
5288
  ]);
3719
- // Common incorrect patterns
3720
- const invalidResultPatterns = new Set(['data', 'rows', 'records', 'items', 'values']);
5289
+ // Map of common incorrect properties to the correct property
5290
+ const incorrectToCorrectMap = {
5291
+ 'data': 'Results',
5292
+ 'Data': 'Results',
5293
+ 'rows': 'Results',
5294
+ 'Rows': 'Results',
5295
+ 'records': 'Results',
5296
+ 'Records': 'Results',
5297
+ 'items': 'Results',
5298
+ 'Items': 'Results',
5299
+ 'values': 'Results',
5300
+ 'Values': 'Results',
5301
+ 'result': 'Results',
5302
+ 'Result': 'Results',
5303
+ 'resultSet': 'Results',
5304
+ 'ResultSet': 'Results',
5305
+ 'dataset': 'Results',
5306
+ 'Dataset': 'Results',
5307
+ 'response': 'Results',
5308
+ 'Response': 'Results'
5309
+ };
3721
5310
  (0, traverse_1.default)(ast, {
3722
5311
  MemberExpression(path) {
3723
5312
  // Check if this is accessing a property on a variable that looks like a query/view result
3724
5313
  if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
3725
5314
  const objName = path.node.object.name;
3726
5315
  const propName = path.node.property.name;
3727
- // Check if the object name suggests it's a query or view result
3728
- const isLikelyQueryResult = /result|response|res|data|output/i.test(objName);
5316
+ // Only check if we can definitively trace this to RunQuery or RunView
3729
5317
  const isFromRunQuery = path.scope.hasBinding(objName) &&
3730
5318
  ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
3731
5319
  const isFromRunView = path.scope.hasBinding(objName) &&
3732
5320
  ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
3733
- if (isLikelyQueryResult || isFromRunQuery || isFromRunView) {
3734
- // Check for common incorrect patterns
3735
- if (invalidResultPatterns.has(propName)) {
3736
- violations.push({
3737
- rule: 'runquery-runview-result-structure',
3738
- severity: 'high',
3739
- line: path.node.loc?.start.line || 0,
3740
- column: path.node.loc?.start.column || 0,
3741
- message: `Incorrect property access "${objName}.${propName}". RunQuery/RunView results use ".Results" for data array, not ".${propName}". Change to "${objName}.Results"`,
3742
- code: `${objName}.${propName}`
3743
- });
5321
+ // Only validate if we're CERTAIN it's from RunQuery or RunView
5322
+ if (isFromRunQuery || isFromRunView) {
5323
+ // WHITELIST APPROACH: Check if the property is valid for the result type
5324
+ const isValidQueryProp = validRunQueryResultProps.has(propName);
5325
+ const isValidViewProp = validRunViewResultProps.has(propName);
5326
+ // If it's specifically from RunQuery or RunView, be more specific
5327
+ if (isFromRunQuery && !isValidQueryProp) {
5328
+ // Property is not valid for RunQueryResult
5329
+ const suggestion = incorrectToCorrectMap[propName];
5330
+ if (suggestion) {
5331
+ violations.push({
5332
+ rule: 'runquery-result-invalid-property',
5333
+ severity: 'critical',
5334
+ line: path.node.loc?.start.line || 0,
5335
+ column: path.node.loc?.start.column || 0,
5336
+ message: `RunQuery results don't have a ".${propName}" property. Use ".${suggestion}" instead. Change "${objName}.${propName}" to "${objName}.${suggestion}"`,
5337
+ code: `${objName}.${propName}`
5338
+ });
5339
+ }
5340
+ else {
5341
+ violations.push({
5342
+ rule: 'runquery-result-invalid-property',
5343
+ severity: 'critical',
5344
+ line: path.node.loc?.start.line || 0,
5345
+ column: path.node.loc?.start.column || 0,
5346
+ message: `Invalid property "${propName}" on RunQuery result. Valid properties are: ${Array.from(validRunQueryResultProps).join(', ')}`,
5347
+ code: `${objName}.${propName}`
5348
+ });
5349
+ }
3744
5350
  }
3745
- else if (propName === 'data') {
3746
- // Special case for .data - very common mistake
3747
- violations.push({
3748
- rule: 'runquery-runview-result-structure',
3749
- severity: 'critical',
3750
- line: path.node.loc?.start.line || 0,
3751
- column: path.node.loc?.start.column || 0,
3752
- message: `RunQuery/RunView results don't have a ".data" property. Use ".Results" to access the array of returned rows. Change "${objName}.data" to "${objName}.Results"`,
3753
- code: `${objName}.${propName}`
3754
- });
5351
+ else if (isFromRunView && !isValidViewProp) {
5352
+ // Property is not valid for RunViewResult
5353
+ const suggestion = incorrectToCorrectMap[propName];
5354
+ if (suggestion) {
5355
+ violations.push({
5356
+ rule: 'runview-result-invalid-property',
5357
+ severity: 'critical',
5358
+ line: path.node.loc?.start.line || 0,
5359
+ column: path.node.loc?.start.column || 0,
5360
+ message: `RunView results don't have a ".${propName}" property. Use ".${suggestion}" instead. Change "${objName}.${propName}" to "${objName}.${suggestion}"`,
5361
+ code: `${objName}.${propName}`
5362
+ });
5363
+ }
5364
+ else {
5365
+ violations.push({
5366
+ rule: 'runview-result-invalid-property',
5367
+ severity: 'critical',
5368
+ line: path.node.loc?.start.line || 0,
5369
+ column: path.node.loc?.start.column || 0,
5370
+ message: `Invalid property "${propName}" on RunView result. Valid properties are: ${Array.from(validRunViewResultProps).join(', ')}`,
5371
+ code: `${objName}.${propName}`
5372
+ });
5373
+ }
3755
5374
  }
3756
- // Check for nested incorrect access like result.data.entities
5375
+ // Check for nested incorrect access like result.data.entities or result.Data.entities
3757
5376
  if (t.isMemberExpression(path.parent) &&
3758
5377
  t.isIdentifier(path.parent.property) &&
3759
- propName === 'data') {
5378
+ (propName === 'data' || propName === 'Data')) {
3760
5379
  const nestedProp = path.parent.property.name;
3761
5380
  violations.push({
3762
5381
  rule: 'runquery-runview-result-structure',
3763
5382
  severity: 'critical',
3764
5383
  line: path.parent.loc?.start.line || 0,
3765
5384
  column: path.parent.loc?.start.column || 0,
3766
- message: `Incorrect nested property access "${objName}.data.${nestedProp}". RunQuery/RunView results use ".Results" directly for the data array. Change to "${objName}.Results"`,
3767
- code: `${objName}.data.${nestedProp}`
5385
+ message: `Incorrect nested property access "${objName}.${propName}.${nestedProp}". RunQuery/RunView results use ".Results" directly for the data array. Change to "${objName}.Results"`,
5386
+ code: `${objName}.${propName}.${nestedProp}`
3768
5387
  });
3769
5388
  }
3770
5389
  }
@@ -3775,29 +5394,42 @@ ComponentLinter.universalComponentRules = [
3775
5394
  // Check for destructuring from a result object
3776
5395
  if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
3777
5396
  const sourceName = path.node.init.name;
3778
- // Check if this looks like destructuring from a query/view result
3779
- if (/result|response|res/i.test(sourceName)) {
5397
+ // Only check if we can definitively trace this to RunQuery or RunView
5398
+ const isFromRunQuery = path.scope.hasBinding(sourceName) &&
5399
+ ComponentLinter.isVariableFromRunQueryOrView(path, sourceName, 'RunQuery');
5400
+ const isFromRunView = path.scope.hasBinding(sourceName) &&
5401
+ ComponentLinter.isVariableFromRunQueryOrView(path, sourceName, 'RunView');
5402
+ // Only validate if we're CERTAIN it's from RunQuery or RunView
5403
+ if (isFromRunQuery || isFromRunView) {
3780
5404
  for (const prop of path.node.id.properties) {
3781
5405
  if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
3782
5406
  const propName = prop.key.name;
3783
- // Check for incorrect destructuring
3784
- if (propName === 'data') {
5407
+ // WHITELIST APPROACH: Check if property is valid
5408
+ const isValidQueryProp = validRunQueryResultProps.has(propName);
5409
+ const isValidViewProp = validRunViewResultProps.has(propName);
5410
+ if (isFromRunQuery && !isValidQueryProp) {
5411
+ const suggestion = incorrectToCorrectMap[propName];
3785
5412
  violations.push({
3786
- rule: 'runquery-runview-result-structure',
5413
+ rule: 'runquery-result-invalid-destructuring',
3787
5414
  severity: 'critical',
3788
5415
  line: prop.loc?.start.line || 0,
3789
5416
  column: prop.loc?.start.column || 0,
3790
- message: `Destructuring "data" from RunQuery/RunView result. The property is named "Results", not "data". Change "const { data } = ${sourceName}" to "const { Results } = ${sourceName}"`,
3791
- code: `{ data }`
5417
+ message: suggestion
5418
+ ? `Destructuring invalid property "${propName}" from RunQuery result. Use "${suggestion}" instead. Change "const { ${propName} } = ${sourceName}" to "const { ${suggestion} } = ${sourceName}"`
5419
+ : `Destructuring invalid property "${propName}" from RunQuery result. Valid properties: ${Array.from(validRunQueryResultProps).join(', ')}`,
5420
+ code: `{ ${propName} }`
3792
5421
  });
3793
5422
  }
3794
- else if (invalidResultPatterns.has(propName) && propName !== 'data') {
5423
+ else if (isFromRunView && !isValidViewProp) {
5424
+ const suggestion = incorrectToCorrectMap[propName];
3795
5425
  violations.push({
3796
- rule: 'runquery-runview-result-structure',
3797
- severity: 'medium',
5426
+ rule: 'runview-result-invalid-destructuring',
5427
+ severity: 'critical',
3798
5428
  line: prop.loc?.start.line || 0,
3799
5429
  column: prop.loc?.start.column || 0,
3800
- message: `Destructuring "${propName}" from what appears to be a RunQuery/RunView result. Did you mean "Results"?`,
5430
+ message: suggestion
5431
+ ? `Destructuring invalid property "${propName}" from RunView result. Use "${suggestion}" instead. Change "const { ${propName} } = ${sourceName}" to "const { ${suggestion} } = ${sourceName}"`
5432
+ : `Destructuring invalid property "${propName}" from RunView result. Valid properties: ${Array.from(validRunViewResultProps).join(', ')}`,
3801
5433
  code: `{ ${propName} }`
3802
5434
  });
3803
5435
  }
@@ -4003,6 +5635,443 @@ ComponentLinter.universalComponentRules = [
4003
5635
  });
4004
5636
  return violations;
4005
5637
  }
5638
+ },
5639
+ {
5640
+ name: 'utilities-valid-properties',
5641
+ appliesTo: 'all',
5642
+ test: (ast, componentName, componentSpec) => {
5643
+ const violations = [];
5644
+ const validProperties = new Set(['rv', 'rq', 'md', 'ai']);
5645
+ (0, traverse_1.default)(ast, {
5646
+ MemberExpression(path) {
5647
+ // Check for utilities.* access
5648
+ if (t.isIdentifier(path.node.object) && path.node.object.name === 'utilities') {
5649
+ if (t.isIdentifier(path.node.property)) {
5650
+ const propName = path.node.property.name;
5651
+ // Check if it's a valid property
5652
+ if (!validProperties.has(propName)) {
5653
+ violations.push({
5654
+ rule: 'utilities-valid-properties',
5655
+ severity: 'critical',
5656
+ line: path.node.loc?.start.line || 0,
5657
+ column: path.node.loc?.start.column || 0,
5658
+ message: `Invalid utilities property '${propName}'. Valid properties are: rv (RunView), rq (RunQuery), md (Metadata), ai (AI Tools)`,
5659
+ code: `utilities.${propName}`
5660
+ });
5661
+ }
5662
+ }
5663
+ }
5664
+ }
5665
+ });
5666
+ return violations;
5667
+ }
5668
+ },
5669
+ {
5670
+ name: 'utilities-runview-methods',
5671
+ appliesTo: 'all',
5672
+ test: (ast, componentName, componentSpec) => {
5673
+ const violations = [];
5674
+ const validMethods = new Set(['RunView', 'RunViews']);
5675
+ (0, traverse_1.default)(ast, {
5676
+ CallExpression(path) {
5677
+ // Check for utilities.rv.* method calls
5678
+ if (t.isMemberExpression(path.node.callee)) {
5679
+ const callee = path.node.callee;
5680
+ // Check if it's utilities.rv.methodName()
5681
+ if (t.isMemberExpression(callee.object) &&
5682
+ t.isIdentifier(callee.object.object) &&
5683
+ callee.object.object.name === 'utilities' &&
5684
+ t.isIdentifier(callee.object.property) &&
5685
+ callee.object.property.name === 'rv' &&
5686
+ t.isIdentifier(callee.property)) {
5687
+ const methodName = callee.property.name;
5688
+ if (!validMethods.has(methodName)) {
5689
+ violations.push({
5690
+ rule: 'utilities-runview-methods',
5691
+ severity: 'critical',
5692
+ line: path.node.loc?.start.line || 0,
5693
+ column: path.node.loc?.start.column || 0,
5694
+ message: `Invalid method '${methodName}' on utilities.rv. Valid methods are: RunView, RunViews`,
5695
+ code: `utilities.rv.${methodName}()`
5696
+ });
5697
+ }
5698
+ }
5699
+ }
5700
+ }
5701
+ });
5702
+ return violations;
5703
+ }
5704
+ },
5705
+ {
5706
+ name: 'utilities-runquery-methods',
5707
+ appliesTo: 'all',
5708
+ test: (ast, componentName, componentSpec) => {
5709
+ const violations = [];
5710
+ const validMethods = new Set(['RunQuery']);
5711
+ (0, traverse_1.default)(ast, {
5712
+ CallExpression(path) {
5713
+ // Check for utilities.rq.* method calls
5714
+ if (t.isMemberExpression(path.node.callee)) {
5715
+ const callee = path.node.callee;
5716
+ // Check if it's utilities.rq.methodName()
5717
+ if (t.isMemberExpression(callee.object) &&
5718
+ t.isIdentifier(callee.object.object) &&
5719
+ callee.object.object.name === 'utilities' &&
5720
+ t.isIdentifier(callee.object.property) &&
5721
+ callee.object.property.name === 'rq' &&
5722
+ t.isIdentifier(callee.property)) {
5723
+ const methodName = callee.property.name;
5724
+ if (!validMethods.has(methodName)) {
5725
+ violations.push({
5726
+ rule: 'utilities-runquery-methods',
5727
+ severity: 'critical',
5728
+ line: path.node.loc?.start.line || 0,
5729
+ column: path.node.loc?.start.column || 0,
5730
+ message: `Invalid method '${methodName}' on utilities.rq. Valid method is: RunQuery`,
5731
+ code: `utilities.rq.${methodName}()`
5732
+ });
5733
+ }
5734
+ }
5735
+ }
5736
+ }
5737
+ });
5738
+ return violations;
5739
+ }
5740
+ },
5741
+ {
5742
+ name: 'utilities-metadata-methods',
5743
+ appliesTo: 'all',
5744
+ test: (ast, componentName, componentSpec) => {
5745
+ const violations = [];
5746
+ const validMethods = new Set(['GetEntityObject']);
5747
+ const validProperties = new Set(['Entities']);
5748
+ (0, traverse_1.default)(ast, {
5749
+ // Check for method calls
5750
+ CallExpression(path) {
5751
+ // Check for utilities.md.* method calls
5752
+ if (t.isMemberExpression(path.node.callee)) {
5753
+ const callee = path.node.callee;
5754
+ // Check if it's utilities.md.methodName()
5755
+ if (t.isMemberExpression(callee.object) &&
5756
+ t.isIdentifier(callee.object.object) &&
5757
+ callee.object.object.name === 'utilities' &&
5758
+ t.isIdentifier(callee.object.property) &&
5759
+ callee.object.property.name === 'md' &&
5760
+ t.isIdentifier(callee.property)) {
5761
+ const methodName = callee.property.name;
5762
+ if (!validMethods.has(methodName)) {
5763
+ violations.push({
5764
+ rule: 'utilities-metadata-methods',
5765
+ severity: 'critical',
5766
+ line: path.node.loc?.start.line || 0,
5767
+ column: path.node.loc?.start.column || 0,
5768
+ message: `Invalid method '${methodName}' on utilities.md. Valid methods are: GetEntityObject. Valid properties are: Entities`,
5769
+ code: `utilities.md.${methodName}()`
5770
+ });
5771
+ }
5772
+ }
5773
+ }
5774
+ },
5775
+ // Check for property access (non-call expressions)
5776
+ MemberExpression(path) {
5777
+ // Skip if this is part of a call expression (handled above)
5778
+ if (t.isCallExpression(path.parent) && path.parent.callee === path.node) {
5779
+ return;
5780
+ }
5781
+ // Check if it's utilities.md.propertyName
5782
+ if (t.isMemberExpression(path.node.object) &&
5783
+ t.isIdentifier(path.node.object.object) &&
5784
+ path.node.object.object.name === 'utilities' &&
5785
+ t.isIdentifier(path.node.object.property) &&
5786
+ path.node.object.property.name === 'md' &&
5787
+ t.isIdentifier(path.node.property)) {
5788
+ const propName = path.node.property.name;
5789
+ // Check if it's accessing a valid property or trying to access an invalid one
5790
+ if (!validProperties.has(propName) && !validMethods.has(propName)) {
5791
+ violations.push({
5792
+ rule: 'utilities-metadata-methods',
5793
+ severity: 'critical',
5794
+ line: path.node.loc?.start.line || 0,
5795
+ column: path.node.loc?.start.column || 0,
5796
+ message: `Invalid property '${propName}' on utilities.md. Valid methods are: GetEntityObject. Valid properties are: Entities`,
5797
+ code: `utilities.md.${propName}`
5798
+ });
5799
+ }
5800
+ }
5801
+ }
5802
+ });
5803
+ return violations;
5804
+ }
5805
+ },
5806
+ {
5807
+ name: 'utilities-ai-methods',
5808
+ appliesTo: 'all',
5809
+ test: (ast, componentName, componentSpec) => {
5810
+ const violations = [];
5811
+ const validMethods = new Set(['ExecutePrompt', 'EmbedText']);
5812
+ const validProperties = new Set(['VectorService']);
5813
+ (0, traverse_1.default)(ast, {
5814
+ // Check for method calls
5815
+ CallExpression(path) {
5816
+ // Check for utilities.ai.* method calls
5817
+ if (t.isMemberExpression(path.node.callee)) {
5818
+ const callee = path.node.callee;
5819
+ // Check if it's utilities.ai.methodName()
5820
+ if (t.isMemberExpression(callee.object) &&
5821
+ t.isIdentifier(callee.object.object) &&
5822
+ callee.object.object.name === 'utilities' &&
5823
+ t.isIdentifier(callee.object.property) &&
5824
+ callee.object.property.name === 'ai' &&
5825
+ t.isIdentifier(callee.property)) {
5826
+ const methodName = callee.property.name;
5827
+ if (!validMethods.has(methodName)) {
5828
+ violations.push({
5829
+ rule: 'utilities-ai-methods',
5830
+ severity: 'critical',
5831
+ line: path.node.loc?.start.line || 0,
5832
+ column: path.node.loc?.start.column || 0,
5833
+ message: `Invalid method '${methodName}' on utilities.ai. Valid methods are: ExecutePrompt, EmbedText. Valid property: VectorService`,
5834
+ code: `utilities.ai.${methodName}()`
5835
+ });
5836
+ }
5837
+ }
5838
+ }
5839
+ },
5840
+ // Check for property access (VectorService)
5841
+ MemberExpression(path) {
5842
+ // Skip if this is part of a call expression (handled above)
5843
+ if (t.isCallExpression(path.parent)) {
5844
+ return;
5845
+ }
5846
+ // Check if it's utilities.ai.propertyName
5847
+ if (t.isMemberExpression(path.node.object) &&
5848
+ t.isIdentifier(path.node.object.object) &&
5849
+ path.node.object.object.name === 'utilities' &&
5850
+ t.isIdentifier(path.node.object.property) &&
5851
+ path.node.object.property.name === 'ai' &&
5852
+ t.isIdentifier(path.node.property)) {
5853
+ const propName = path.node.property.name;
5854
+ // Check if it's a valid property or method (methods might be referenced without calling)
5855
+ if (!validProperties.has(propName) && !validMethods.has(propName)) {
5856
+ violations.push({
5857
+ rule: 'utilities-ai-properties',
5858
+ severity: 'critical',
5859
+ line: path.node.loc?.start.line || 0,
5860
+ column: path.node.loc?.start.column || 0,
5861
+ message: `Invalid property '${propName}' on utilities.ai. Valid methods are: ExecutePrompt, EmbedText. Valid property: VectorService`,
5862
+ code: `utilities.ai.${propName}`
5863
+ });
5864
+ }
5865
+ }
5866
+ }
5867
+ });
5868
+ return violations;
5869
+ }
5870
+ },
5871
+ {
5872
+ name: 'utilities-no-direct-instantiation',
5873
+ appliesTo: 'all',
5874
+ test: (ast, componentName, componentSpec) => {
5875
+ const violations = [];
5876
+ const restrictedClasses = new Map([
5877
+ ['RunView', 'utilities.rv'],
5878
+ ['RunQuery', 'utilities.rq'],
5879
+ ['Metadata', 'utilities.md'],
5880
+ ['SimpleVectorService', 'utilities.ai.VectorService']
5881
+ ]);
5882
+ (0, traverse_1.default)(ast, {
5883
+ NewExpression(path) {
5884
+ // Check if instantiating a restricted class
5885
+ if (t.isIdentifier(path.node.callee)) {
5886
+ const className = path.node.callee.name;
5887
+ if (restrictedClasses.has(className)) {
5888
+ const utilityPath = restrictedClasses.get(className);
5889
+ violations.push({
5890
+ rule: 'utilities-no-direct-instantiation',
5891
+ severity: 'high',
5892
+ line: path.node.loc?.start.line || 0,
5893
+ column: path.node.loc?.start.column || 0,
5894
+ message: `Don't instantiate ${className} directly. Use ${utilityPath} instead which is provided in the component's utilities parameter.`,
5895
+ code: `new ${className}()`
5896
+ });
5897
+ }
5898
+ }
5899
+ }
5900
+ });
5901
+ return violations;
5902
+ }
5903
+ },
5904
+ {
5905
+ name: 'unsafe-formatting-methods',
5906
+ appliesTo: 'all',
5907
+ test: (ast, componentName, componentSpec, options) => {
5908
+ const violations = [];
5909
+ // Common formatting methods that can fail on null/undefined
5910
+ const formattingMethods = new Set([
5911
+ // Number methods
5912
+ 'toFixed', 'toPrecision', 'toExponential',
5913
+ // Conversion methods
5914
+ 'toLocaleString', 'toString',
5915
+ // String methods
5916
+ 'toLowerCase', 'toUpperCase', 'trim',
5917
+ 'split', 'slice', 'substring', 'substr',
5918
+ 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf',
5919
+ 'padStart', 'padEnd', 'repeat', 'replace'
5920
+ ]);
5921
+ const checkFieldNullability = (propertyName) => {
5922
+ // Step 1: Check if componentSpec has data requirements and utilities are available
5923
+ if (!componentSpec?.dataRequirements?.entities || !options?.utilities?.md?.Entities) {
5924
+ return { found: false, nullable: false };
5925
+ }
5926
+ try {
5927
+ // Step 2: Iterate through only the entities defined in dataRequirements
5928
+ for (const dataReqEntity of componentSpec.dataRequirements.entities) {
5929
+ const entityName = dataReqEntity.name; // e.g., "AI Prompt Runs"
5930
+ // Step 3: Find this entity in the full metadata (case insensitive)
5931
+ // Use proper typing - we know Entities is an array of EntityInfo objects
5932
+ const fullEntity = options.utilities.md?.Entities.find((e) => e.Name && e.Name.toLowerCase() === entityName.toLowerCase());
5933
+ if (fullEntity && fullEntity.Fields && Array.isArray(fullEntity.Fields)) {
5934
+ // Step 4: Look for the field in this specific entity (case insensitive)
5935
+ const field = fullEntity.Fields.find((f) => f.Name && f.Name.trim().toLowerCase() === propertyName.trim().toLowerCase());
5936
+ if (field) {
5937
+ // Field found - check if it's nullable
5938
+ // In MJ, AllowsNull is a boolean property
5939
+ return {
5940
+ found: true,
5941
+ nullable: field.AllowsNull,
5942
+ entityName: fullEntity.Name,
5943
+ fieldName: field.Name
5944
+ };
5945
+ }
5946
+ }
5947
+ }
5948
+ }
5949
+ catch (error) {
5950
+ // If there's any error accessing metadata, fail gracefully
5951
+ console.warn('Error checking field nullability:', error);
5952
+ }
5953
+ return { found: false, nullable: false };
5954
+ };
5955
+ (0, traverse_1.default)(ast, {
5956
+ // Check JSX expressions
5957
+ JSXExpressionContainer(path) {
5958
+ const expr = path.node.expression;
5959
+ // Look for object.property.method() pattern
5960
+ if (t.isCallExpression(expr) &&
5961
+ t.isMemberExpression(expr.callee) &&
5962
+ t.isIdentifier(expr.callee.property)) {
5963
+ const methodName = expr.callee.property.name;
5964
+ // Check if it's a formatting method
5965
+ if (formattingMethods.has(methodName)) {
5966
+ const callee = expr.callee;
5967
+ // Check if the object being called on is also a member expression (x.y pattern)
5968
+ if (t.isMemberExpression(callee.object) &&
5969
+ t.isIdentifier(callee.object.property)) {
5970
+ const propertyName = callee.object.property.name;
5971
+ // Check if optional chaining is already used
5972
+ const hasOptionalChaining = callee.object.optional || callee.optional;
5973
+ // Check if there's a fallback (looking in parent for || or ??)
5974
+ let hasFallback = false;
5975
+ const parent = path.parent;
5976
+ const grandParent = path.parentPath?.parent;
5977
+ // Check if parent is a logical expression with fallback
5978
+ if (grandParent && t.isLogicalExpression(grandParent) &&
5979
+ (grandParent.operator === '||' || grandParent.operator === '??')) {
5980
+ hasFallback = true;
5981
+ }
5982
+ // Also check conditional expressions
5983
+ if (grandParent && t.isConditionalExpression(grandParent)) {
5984
+ hasFallback = true;
5985
+ }
5986
+ if (!hasOptionalChaining && !hasFallback) {
5987
+ // Check entity metadata for this field
5988
+ const fieldInfo = checkFieldNullability(propertyName);
5989
+ // Determine severity based on metadata
5990
+ let severity = 'medium';
5991
+ let message = `Unsafe formatting method '${methodName}()' called on '${propertyName}'. Consider using optional chaining.`;
5992
+ if (fieldInfo.found) {
5993
+ if (fieldInfo.nullable) {
5994
+ severity = 'high';
5995
+ message = `Field '${fieldInfo.fieldName}' from entity '${fieldInfo.entityName}' is nullable. Use optional chaining to prevent runtime errors when calling '${methodName}()'.`;
5996
+ }
5997
+ else {
5998
+ // Keep medium severity but note it's non-nullable
5999
+ message = `Field '${fieldInfo.fieldName}' from entity '${fieldInfo.entityName}' appears to be non-nullable, but consider using optional chaining for safety when calling '${methodName}()'.`;
6000
+ }
6001
+ }
6002
+ // Get the object name for better error message
6003
+ let objectName = '';
6004
+ if (t.isIdentifier(callee.object.object)) {
6005
+ objectName = callee.object.object.name;
6006
+ }
6007
+ violations.push({
6008
+ rule: 'unsafe-formatting-methods',
6009
+ severity: severity,
6010
+ line: expr.loc?.start.line || 0,
6011
+ column: expr.loc?.start.column || 0,
6012
+ message: message,
6013
+ code: `${objectName}.${propertyName}.${methodName}() → ${objectName}.${propertyName}?.${methodName}() ?? defaultValue`
6014
+ });
6015
+ }
6016
+ }
6017
+ }
6018
+ }
6019
+ },
6020
+ // Also check template literals
6021
+ TemplateLiteral(path) {
6022
+ for (const expr of path.node.expressions) {
6023
+ // Look for object.property.method() pattern in template expressions
6024
+ if (t.isCallExpression(expr) &&
6025
+ t.isMemberExpression(expr.callee) &&
6026
+ t.isIdentifier(expr.callee.property)) {
6027
+ const methodName = expr.callee.property.name;
6028
+ // Check if it's a formatting method
6029
+ if (formattingMethods.has(methodName)) {
6030
+ const callee = expr.callee;
6031
+ // Check if the object being called on is also a member expression (x.y pattern)
6032
+ if (t.isMemberExpression(callee.object) &&
6033
+ t.isIdentifier(callee.object.property)) {
6034
+ const propertyName = callee.object.property.name;
6035
+ // Check if optional chaining is already used
6036
+ const hasOptionalChaining = callee.object.optional || callee.optional;
6037
+ if (!hasOptionalChaining) {
6038
+ // Check entity metadata for this field
6039
+ const fieldInfo = checkFieldNullability(propertyName);
6040
+ // Determine severity based on metadata
6041
+ let severity = 'medium';
6042
+ let message = `Unsafe formatting method '${methodName}()' called on '${propertyName}' in template literal. Consider using optional chaining.`;
6043
+ if (fieldInfo.found) {
6044
+ if (fieldInfo.nullable) {
6045
+ severity = 'high';
6046
+ message = `Field '${propertyName}' is nullable in entity metadata. Use optional chaining to prevent runtime errors when calling '${methodName}()' in template literal.`;
6047
+ }
6048
+ else {
6049
+ // Keep medium severity but note it's non-nullable
6050
+ message = `Field '${propertyName}' appears to be non-nullable, but consider using optional chaining for safety when calling '${methodName}()' in template literal.`;
6051
+ }
6052
+ }
6053
+ // Get the object name for better error message
6054
+ let objectName = '';
6055
+ if (t.isIdentifier(callee.object.object)) {
6056
+ objectName = callee.object.object.name;
6057
+ }
6058
+ violations.push({
6059
+ rule: 'unsafe-formatting-methods',
6060
+ severity: severity,
6061
+ line: expr.loc?.start.line || 0,
6062
+ column: expr.loc?.start.column || 0,
6063
+ message: message,
6064
+ code: `\${${objectName}.${propertyName}.${methodName}()} → \${${objectName}.${propertyName}?.${methodName}() ?? defaultValue}`
6065
+ });
6066
+ }
6067
+ }
6068
+ }
6069
+ }
6070
+ }
6071
+ }
6072
+ });
6073
+ return violations;
6074
+ }
4006
6075
  }
4007
6076
  ];
4008
6077
  //# sourceMappingURL=component-linter.js.map