@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/component-linter.d.ts +31 -1
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +2552 -483
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +5 -2
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +305 -50
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/library-lint-cache.d.ts +46 -0
- package/dist/lib/library-lint-cache.d.ts.map +1 -0
- package/dist/lib/library-lint-cache.js +119 -0
- package/dist/lib/library-lint-cache.js.map +1 -0
- package/dist/lib/linter-test-tool.d.ts +42 -0
- package/dist/lib/linter-test-tool.d.ts.map +1 -0
- package/dist/lib/linter-test-tool.js +272 -0
- package/dist/lib/linter-test-tool.js.map +1 -0
- package/dist/lib/test-harness.d.ts.map +1 -1
- package/dist/lib/test-harness.js +1 -1
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -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 '
|
|
1506
|
+
case 'component-props-validation':
|
|
1454
1507
|
suggestions.push({
|
|
1455
1508
|
violation: violation.rule,
|
|
1456
|
-
suggestion: '
|
|
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: '
|
|
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.
|
|
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: '
|
|
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.
|
|
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
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
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: '
|
|
3412
|
+
name: 'react-hooks-rules',
|
|
2662
3413
|
appliesTo: 'all',
|
|
2663
3414
|
test: (ast, componentName, componentSpec) => {
|
|
2664
3415
|
const violations = [];
|
|
2665
|
-
const
|
|
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
|
|
2670
|
-
|
|
2671
|
-
if
|
|
2672
|
-
|
|
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
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
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: '
|
|
2724
|
-
severity: '
|
|
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:
|
|
2728
|
-
code:
|
|
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
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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: '
|
|
3047
|
-
appliesTo: '
|
|
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
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
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
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
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
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
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 (
|
|
3103
|
-
|
|
3104
|
-
|
|
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
|
|
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
|
|
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
|
|
4593
|
+
// Check for direct array access patterns like arr[0]
|
|
3227
4594
|
MemberExpression(path) {
|
|
3228
|
-
const { object, property } = path.node;
|
|
3229
|
-
// Check for array
|
|
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: `
|
|
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
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
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
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
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
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
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
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
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
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
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
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
//
|
|
3415
|
-
const
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
const
|
|
3420
|
-
if
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
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: '
|
|
5052
|
+
rule: 'runquery-runview-validation',
|
|
3444
5053
|
severity: 'critical',
|
|
3445
|
-
line:
|
|
3446
|
-
column:
|
|
3447
|
-
message: `
|
|
3448
|
-
code:
|
|
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
|
-
|
|
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: '
|
|
3524
|
-
severity: '
|
|
3525
|
-
line:
|
|
3526
|
-
column:
|
|
3527
|
-
message: `
|
|
3528
|
-
code:
|
|
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 (
|
|
3533
|
-
//
|
|
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: '
|
|
3536
|
-
severity: '
|
|
3537
|
-
line:
|
|
3538
|
-
column:
|
|
3539
|
-
message: `
|
|
3540
|
-
code:
|
|
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
|
-
|
|
3544
|
-
|
|
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: '
|
|
5100
|
+
rule: 'runquery-runview-validation',
|
|
3547
5101
|
severity: 'high',
|
|
3548
|
-
line:
|
|
3549
|
-
column:
|
|
3550
|
-
message: `
|
|
3551
|
-
code:
|
|
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
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
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',
|
|
3712
|
-
'
|
|
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',
|
|
3717
|
-
'
|
|
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
|
-
//
|
|
3720
|
-
const
|
|
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
|
-
//
|
|
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
|
|
3734
|
-
|
|
3735
|
-
if
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
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 (
|
|
3746
|
-
//
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
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}
|
|
3767
|
-
code: `${objName}
|
|
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
|
-
//
|
|
3779
|
-
|
|
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
|
|
3784
|
-
|
|
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-
|
|
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:
|
|
3791
|
-
|
|
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 (
|
|
5423
|
+
else if (isFromRunView && !isValidViewProp) {
|
|
5424
|
+
const suggestion = incorrectToCorrectMap[propName];
|
|
3795
5425
|
violations.push({
|
|
3796
|
-
rule: '
|
|
3797
|
-
severity: '
|
|
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:
|
|
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
|