@memberjunction/react-test-harness 2.97.0 → 2.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +537 -130
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +2 -2
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +394 -45
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/library-lint-cache.d.ts +4 -0
- package/dist/lib/library-lint-cache.d.ts.map +1 -1
- package/dist/lib/library-lint-cache.js +61 -3
- package/dist/lib/library-lint-cache.js.map +1 -1
- package/dist/lib/test-harness.d.ts.map +1 -1
- package/dist/lib/test-harness.js +5 -2
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -196,6 +196,10 @@ class ComponentLinter {
|
|
|
196
196
|
}
|
|
197
197
|
static async lintComponent(code, componentName, componentSpec, isRootComponent, contextUser, debugMode, options) {
|
|
198
198
|
try {
|
|
199
|
+
// Require contextUser when libraries need to be checked
|
|
200
|
+
if (componentSpec?.libraries && componentSpec.libraries.length > 0 && !contextUser) {
|
|
201
|
+
throw new Error('contextUser is required when linting components with library dependencies. This is needed to load library-specific lint rules from the database.');
|
|
202
|
+
}
|
|
199
203
|
// Parse with error recovery to get both AST and errors
|
|
200
204
|
const parseResult = parser.parse(code, {
|
|
201
205
|
sourceType: 'module',
|
|
@@ -259,7 +263,7 @@ class ComponentLinter {
|
|
|
259
263
|
violations.push(...dataViolations);
|
|
260
264
|
}
|
|
261
265
|
// Apply library-specific lint rules if available
|
|
262
|
-
if (componentSpec?.libraries
|
|
266
|
+
if (componentSpec?.libraries) {
|
|
263
267
|
const libraryViolations = await this.applyLibraryLintRules(ast, componentSpec, contextUser, debugMode);
|
|
264
268
|
violations.push(...libraryViolations);
|
|
265
269
|
}
|
|
@@ -1580,34 +1584,24 @@ await utilities.rq.RunQuery({
|
|
|
1580
1584
|
break;
|
|
1581
1585
|
case 'component-props-validation':
|
|
1582
1586
|
violation.suggestion = {
|
|
1583
|
-
text: 'Components can only accept standard props and props explicitly defined in the component spec.
|
|
1587
|
+
text: 'Components can only accept standard props and props explicitly defined in the component spec. The spec is provided by the architect and cannot be modified - your code must match the spec exactly.',
|
|
1584
1588
|
example: `// ❌ WRONG - Component with undeclared props:
|
|
1585
1589
|
function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
|
|
1586
|
-
// customers, orders, selectedId are NOT
|
|
1590
|
+
// ERROR: customers, orders, selectedId are NOT in the spec
|
|
1591
|
+
// The spec defines what props are allowed - you cannot add new ones
|
|
1587
1592
|
}
|
|
1588
1593
|
|
|
1589
|
-
// ✅ CORRECT
|
|
1594
|
+
// ✅ CORRECT - Use only standard props and props defined in the spec:
|
|
1590
1595
|
function MyComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
|
|
1591
|
-
//
|
|
1596
|
+
// If you need data like customers/orders, load it internally using utilities
|
|
1592
1597
|
const [customers, setCustomers] = useState([]);
|
|
1593
1598
|
const [orders, setOrders] = useState([]);
|
|
1594
1599
|
const [selectedId, setSelectedId] = useState(savedUserSettings?.selectedId);
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// ✅ CORRECT Option 2 - Define props in component spec:
|
|
1598
|
-
// In spec.properties array:
|
|
1599
|
-
// [
|
|
1600
|
-
// { name: "customers", type: "array", required: false, description: "Customer list" },
|
|
1601
|
-
// { name: "orders", type: "array", required: false, description: "Order list" },
|
|
1602
|
-
// { name: "selectedId", type: "string", required: false, description: "Selected item ID" }
|
|
1603
|
-
// ]
|
|
1604
|
-
// Then the component can accept them:
|
|
1605
|
-
function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
|
|
1606
|
-
// These props are now allowed because they're defined in the spec
|
|
1607
1600
|
|
|
1608
1601
|
useEffect(() => {
|
|
1609
1602
|
const loadData = async () => {
|
|
1610
1603
|
try {
|
|
1604
|
+
// Load customers data internally
|
|
1611
1605
|
const result = await utilities.rv.RunView({
|
|
1612
1606
|
EntityName: 'Customers',
|
|
1613
1607
|
Fields: ['ID', 'Name', 'Status']
|
|
@@ -1622,8 +1616,12 @@ function MyComponent({ utilities, styles, components, customers, orders, selecte
|
|
|
1622
1616
|
loadData();
|
|
1623
1617
|
}, []);
|
|
1624
1618
|
|
|
1625
|
-
return <div>{/* Use state, not props */}</div>;
|
|
1626
|
-
}
|
|
1619
|
+
return <div>{/* Use state variables, not props */}</div>;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// NOTE: If the spec DOES define additional props (e.g., customers, orders),
|
|
1623
|
+
// then you MUST accept and use them. Check the spec's properties array
|
|
1624
|
+
// to see what props are required/optional beyond the standard ones.`
|
|
1627
1625
|
};
|
|
1628
1626
|
break;
|
|
1629
1627
|
case 'runview-runquery-result-direct-usage':
|
|
@@ -1762,50 +1760,78 @@ const {
|
|
|
1762
1760
|
const libraryViolations = [];
|
|
1763
1761
|
// Get the cached and compiled rules for this library
|
|
1764
1762
|
const compiledRules = cache.getLibraryRules(lib.name);
|
|
1763
|
+
if (debugMode) {
|
|
1764
|
+
console.log(`\n 📚 Library: ${lib.name}`);
|
|
1765
|
+
if (compiledRules) {
|
|
1766
|
+
console.log(` ┌─ Has lint rules: ✅`);
|
|
1767
|
+
if (compiledRules.validators) {
|
|
1768
|
+
console.log(` ├─ Validators: ${Object.keys(compiledRules.validators).length}`);
|
|
1769
|
+
}
|
|
1770
|
+
if (compiledRules.initialization) {
|
|
1771
|
+
console.log(` ├─ Initialization rules: ✅`);
|
|
1772
|
+
}
|
|
1773
|
+
if (compiledRules.lifecycle) {
|
|
1774
|
+
console.log(` ├─ Lifecycle rules: ✅`);
|
|
1775
|
+
}
|
|
1776
|
+
console.log(` └─ Starting checks...`);
|
|
1777
|
+
}
|
|
1778
|
+
else {
|
|
1779
|
+
console.log(` └─ No lint rules defined`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1765
1782
|
if (compiledRules) {
|
|
1766
1783
|
const library = compiledRules.library;
|
|
1767
1784
|
const libraryName = library.Name || lib.name;
|
|
1768
1785
|
// Apply initialization rules
|
|
1769
1786
|
if (compiledRules.initialization) {
|
|
1787
|
+
if (debugMode) {
|
|
1788
|
+
console.log(` ├─ 🔍 Checking ${libraryName} initialization patterns...`);
|
|
1789
|
+
}
|
|
1770
1790
|
const initViolations = this.checkLibraryInitialization(ast, libraryName, compiledRules.initialization);
|
|
1771
1791
|
// Debug logging for library violations
|
|
1772
1792
|
if (debugMode && initViolations.length > 0) {
|
|
1773
|
-
console.log(
|
|
1793
|
+
console.log(` │ ⚠️ Found ${initViolations.length} initialization issue${initViolations.length > 1 ? 's' : ''}`);
|
|
1774
1794
|
initViolations.forEach(v => {
|
|
1775
1795
|
const icon = v.severity === 'critical' ? '🔴' :
|
|
1776
1796
|
v.severity === 'high' ? '🟠' :
|
|
1777
1797
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
1778
|
-
console.log(` ${icon}
|
|
1798
|
+
console.log(` │ ${icon} Line ${v.line}: ${v.message}`);
|
|
1779
1799
|
});
|
|
1780
1800
|
}
|
|
1781
1801
|
libraryViolations.push(...initViolations);
|
|
1782
1802
|
}
|
|
1783
1803
|
// Apply lifecycle rules
|
|
1784
1804
|
if (compiledRules.lifecycle) {
|
|
1805
|
+
if (debugMode) {
|
|
1806
|
+
console.log(` ├─ 🔄 Checking ${libraryName} lifecycle management...`);
|
|
1807
|
+
}
|
|
1785
1808
|
const lifecycleViolations = this.checkLibraryLifecycle(ast, libraryName, compiledRules.lifecycle);
|
|
1786
1809
|
// Debug logging for library violations
|
|
1787
1810
|
if (debugMode && lifecycleViolations.length > 0) {
|
|
1788
|
-
console.log(
|
|
1811
|
+
console.log(` │ ⚠️ Found ${lifecycleViolations.length} lifecycle issue${lifecycleViolations.length > 1 ? 's' : ''}`);
|
|
1789
1812
|
lifecycleViolations.forEach(v => {
|
|
1790
1813
|
const icon = v.severity === 'critical' ? '🔴' :
|
|
1791
1814
|
v.severity === 'high' ? '🟠' :
|
|
1792
1815
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
1793
|
-
console.log(` ${icon}
|
|
1816
|
+
console.log(` │ ${icon} Line ${v.line}: ${v.message}`);
|
|
1794
1817
|
});
|
|
1795
1818
|
}
|
|
1796
1819
|
libraryViolations.push(...lifecycleViolations);
|
|
1797
1820
|
}
|
|
1798
1821
|
// Apply options validation
|
|
1799
1822
|
if (compiledRules.options) {
|
|
1823
|
+
if (debugMode) {
|
|
1824
|
+
console.log(` ├─ ⚙️ Checking ${libraryName} configuration options...`);
|
|
1825
|
+
}
|
|
1800
1826
|
const optionsViolations = this.checkLibraryOptions(ast, libraryName, compiledRules.options);
|
|
1801
1827
|
// Debug logging for library violations
|
|
1802
1828
|
if (debugMode && optionsViolations.length > 0) {
|
|
1803
|
-
console.log(
|
|
1829
|
+
console.log(` │ ⚠️ Found ${optionsViolations.length} configuration issue${optionsViolations.length > 1 ? 's' : ''}`);
|
|
1804
1830
|
optionsViolations.forEach(v => {
|
|
1805
1831
|
const icon = v.severity === 'critical' ? '🔴' :
|
|
1806
1832
|
v.severity === 'high' ? '🟠' :
|
|
1807
1833
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
1808
|
-
console.log(` ${icon}
|
|
1834
|
+
console.log(` │ ${icon} Line ${v.line}: ${v.message}`);
|
|
1809
1835
|
});
|
|
1810
1836
|
}
|
|
1811
1837
|
libraryViolations.push(...optionsViolations);
|
|
@@ -2105,6 +2131,13 @@ const {
|
|
|
2105
2131
|
for (const [validatorName, validator] of Object.entries(validators)) {
|
|
2106
2132
|
if (validator && validator.validateFn) {
|
|
2107
2133
|
const beforeCount = context.violations.length;
|
|
2134
|
+
// Log that we're running this specific validator
|
|
2135
|
+
if (debugMode) {
|
|
2136
|
+
console.log(` ├─ 🔬 Running ${libraryName} validator: ${validatorName}`);
|
|
2137
|
+
if (validator.description) {
|
|
2138
|
+
console.log(` │ ℹ️ ${validator.description}`);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2108
2141
|
// Traverse AST and apply validator
|
|
2109
2142
|
(0, traverse_1.default)(ast, {
|
|
2110
2143
|
enter(path) {
|
|
@@ -2115,27 +2148,31 @@ const {
|
|
|
2115
2148
|
catch (error) {
|
|
2116
2149
|
// Validator execution error - log but don't crash
|
|
2117
2150
|
console.warn(`Validator ${validatorName} failed:`, error);
|
|
2151
|
+
if (debugMode) {
|
|
2152
|
+
console.error('Full error:', error);
|
|
2153
|
+
}
|
|
2118
2154
|
}
|
|
2119
2155
|
}
|
|
2120
2156
|
});
|
|
2121
2157
|
// Debug logging for this specific validator
|
|
2122
2158
|
const newViolations = context.violations.length - beforeCount;
|
|
2123
2159
|
if (debugMode && newViolations > 0) {
|
|
2124
|
-
console.log(
|
|
2125
|
-
console.log(` 📊 ${validator.description || 'No description'}`);
|
|
2126
|
-
console.log(` ⚠️ Found ${newViolations} violation${newViolations > 1 ? 's' : ''}`);
|
|
2160
|
+
console.log(` │ ✓ Found ${newViolations} violation${newViolations > 1 ? 's' : ''}`);
|
|
2127
2161
|
// Show the violations from this validator
|
|
2128
2162
|
const validatorViolations = context.violations.slice(beforeCount);
|
|
2129
2163
|
validatorViolations.forEach((v) => {
|
|
2130
2164
|
const icon = v.type === 'error' || v.severity === 'critical' ? '🔴' :
|
|
2131
2165
|
v.type === 'warning' || v.severity === 'high' ? '🟠' :
|
|
2132
2166
|
v.severity === 'medium' ? '🟡' : '🟢';
|
|
2133
|
-
console.log(`
|
|
2167
|
+
console.log(` │ ${icon} Line ${v.line || 'unknown'}: ${v.message}`);
|
|
2134
2168
|
if (v.suggestion) {
|
|
2135
|
-
console.log(`
|
|
2169
|
+
console.log(` │ 💡 ${v.suggestion}`);
|
|
2136
2170
|
}
|
|
2137
2171
|
});
|
|
2138
2172
|
}
|
|
2173
|
+
else if (debugMode) {
|
|
2174
|
+
console.log(` │ ✓ No violations found`);
|
|
2175
|
+
}
|
|
2139
2176
|
}
|
|
2140
2177
|
}
|
|
2141
2178
|
// Convert context violations to standard format
|
|
@@ -2635,7 +2672,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2635
2672
|
severity: 'critical',
|
|
2636
2673
|
line: path.node.loc?.start.line || 0,
|
|
2637
2674
|
column: path.node.loc?.start.column || 0,
|
|
2638
|
-
message: `Component "${componentName}" is trying to
|
|
2675
|
+
message: `Component "${componentName}" is trying to access window.${propertyName}. Libraries must be accessed using unwrapComponents, not through the window object. If this library is in your spec, use: const { ... } = unwrapComponents(${propertyName}, [...]); If it's not in your spec, you cannot use it.`,
|
|
2639
2676
|
code: path.toString().substring(0, 100)
|
|
2640
2677
|
});
|
|
2641
2678
|
}
|
|
@@ -2873,6 +2910,66 @@ ComponentLinter.universalComponentRules = [
|
|
|
2873
2910
|
return violations;
|
|
2874
2911
|
}
|
|
2875
2912
|
},
|
|
2913
|
+
{
|
|
2914
|
+
name: 'use-unwrap-components',
|
|
2915
|
+
appliesTo: 'all',
|
|
2916
|
+
test: (ast, componentName, componentSpec) => {
|
|
2917
|
+
const violations = [];
|
|
2918
|
+
// Build a set of library global variables
|
|
2919
|
+
const libraryGlobals = new Set();
|
|
2920
|
+
if (componentSpec?.libraries) {
|
|
2921
|
+
for (const lib of componentSpec.libraries) {
|
|
2922
|
+
if (lib.globalVariable) {
|
|
2923
|
+
libraryGlobals.add(lib.globalVariable);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
(0, traverse_1.default)(ast, {
|
|
2928
|
+
VariableDeclarator(path) {
|
|
2929
|
+
// Check for direct destructuring from library globals
|
|
2930
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
2931
|
+
const sourceVar = path.node.init.name;
|
|
2932
|
+
// Check if this is destructuring from a library global
|
|
2933
|
+
if (libraryGlobals.has(sourceVar)) {
|
|
2934
|
+
// Extract the destructured component names
|
|
2935
|
+
const componentNames = [];
|
|
2936
|
+
for (const prop of path.node.id.properties) {
|
|
2937
|
+
if (t.isObjectProperty(prop)) {
|
|
2938
|
+
if (t.isIdentifier(prop.key)) {
|
|
2939
|
+
componentNames.push(prop.key.name);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
violations.push({
|
|
2944
|
+
rule: 'use-unwrap-components',
|
|
2945
|
+
severity: 'critical',
|
|
2946
|
+
line: path.node.loc?.start.line || 0,
|
|
2947
|
+
column: path.node.loc?.start.column || 0,
|
|
2948
|
+
message: `Direct destructuring from library "${sourceVar}" is not allowed. You MUST use unwrapComponents to access library components. Replace "const { ${componentNames.join(', ')} } = ${sourceVar};" with "const { ${componentNames.join(', ')} } = unwrapComponents(${sourceVar}, [${componentNames.map(n => `'${n}'`).join(', ')}]);"`
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
// Also check for MemberExpression destructuring like const { Button } = antd.Button
|
|
2953
|
+
if (t.isObjectPattern(path.node.id) && t.isMemberExpression(path.node.init)) {
|
|
2954
|
+
const memberExpr = path.node.init;
|
|
2955
|
+
if (t.isIdentifier(memberExpr.object)) {
|
|
2956
|
+
const objName = memberExpr.object.name;
|
|
2957
|
+
if (libraryGlobals.has(objName)) {
|
|
2958
|
+
violations.push({
|
|
2959
|
+
rule: 'use-unwrap-components',
|
|
2960
|
+
severity: 'critical',
|
|
2961
|
+
line: path.node.loc?.start.line || 0,
|
|
2962
|
+
column: path.node.loc?.start.column || 0,
|
|
2963
|
+
message: `Direct destructuring from library member expression is not allowed. Use unwrapComponents to safely access library components. Example: Instead of "const { Something } = ${objName}.Something;", use "const { Something } = unwrapComponents(${objName}, ['Something']);"`
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
return violations;
|
|
2971
|
+
}
|
|
2972
|
+
},
|
|
2876
2973
|
{
|
|
2877
2974
|
name: 'library-variable-names',
|
|
2878
2975
|
appliesTo: 'all',
|
|
@@ -2905,7 +3002,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2905
3002
|
severity: 'critical',
|
|
2906
3003
|
line: path.node.loc?.start.line || 0,
|
|
2907
3004
|
column: path.node.loc?.start.column || 0,
|
|
2908
|
-
message: `Incorrect library global variable "${sourceVar}". Use
|
|
3005
|
+
message: `Incorrect library global variable "${sourceVar}". Use unwrapComponents with the correct global: "const { ... } = unwrapComponents(${correctGlobal}, [...]);"`
|
|
2909
3006
|
});
|
|
2910
3007
|
}
|
|
2911
3008
|
}
|
|
@@ -4919,6 +5016,92 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4919
5016
|
return violations;
|
|
4920
5017
|
}
|
|
4921
5018
|
},
|
|
5019
|
+
{
|
|
5020
|
+
name: 'string-replace-all-occurrences',
|
|
5021
|
+
appliesTo: 'all',
|
|
5022
|
+
test: (ast, componentName, componentSpec) => {
|
|
5023
|
+
const violations = [];
|
|
5024
|
+
// Template patterns that are HIGH severity (likely to have multiple occurrences)
|
|
5025
|
+
const templatePatterns = [
|
|
5026
|
+
{ pattern: /\{\{[^}]+\}\}/, example: '{{field}}', desc: 'double curly braces' },
|
|
5027
|
+
{ pattern: /\{[^}]+\}/, example: '{field}', desc: 'single curly braces' },
|
|
5028
|
+
{ pattern: /<<[^>]+>>/, example: '<<field>>', desc: 'double angle brackets' },
|
|
5029
|
+
{ pattern: /<[^>]+>/, example: '<field>', desc: 'single angle brackets' }
|
|
5030
|
+
];
|
|
5031
|
+
(0, traverse_1.default)(ast, {
|
|
5032
|
+
CallExpression(path) {
|
|
5033
|
+
const callee = path.node.callee;
|
|
5034
|
+
// Check if it's a .replace() method call
|
|
5035
|
+
if (t.isMemberExpression(callee) &&
|
|
5036
|
+
t.isIdentifier(callee.property) &&
|
|
5037
|
+
callee.property.name === 'replace') {
|
|
5038
|
+
const args = path.node.arguments;
|
|
5039
|
+
if (args.length >= 2) {
|
|
5040
|
+
const [searchArg, replaceArg] = args;
|
|
5041
|
+
// Handle string literal search patterns
|
|
5042
|
+
if (t.isStringLiteral(searchArg)) {
|
|
5043
|
+
const searchValue = searchArg.value;
|
|
5044
|
+
// Check if it matches any template pattern
|
|
5045
|
+
let matchedPattern = null;
|
|
5046
|
+
for (const tp of templatePatterns) {
|
|
5047
|
+
if (tp.pattern.test(searchValue)) {
|
|
5048
|
+
matchedPattern = tp;
|
|
5049
|
+
break;
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
if (matchedPattern) {
|
|
5053
|
+
// HIGH severity for template patterns
|
|
5054
|
+
violations.push({
|
|
5055
|
+
rule: 'string-replace-all-occurrences',
|
|
5056
|
+
severity: 'high',
|
|
5057
|
+
line: path.node.loc?.start.line || 0,
|
|
5058
|
+
column: path.node.loc?.start.column || 0,
|
|
5059
|
+
message: `Using replace() with ${matchedPattern.desc} template '${searchValue}' only replaces the first occurrence. This will cause bugs if the template appears multiple times.`,
|
|
5060
|
+
suggestion: {
|
|
5061
|
+
text: `Use .replaceAll('${searchValue}', ...) to replace all occurrences`,
|
|
5062
|
+
example: `str.replaceAll('${searchValue}', value)`
|
|
5063
|
+
}
|
|
5064
|
+
});
|
|
5065
|
+
}
|
|
5066
|
+
else {
|
|
5067
|
+
// LOW severity for general replace() usage
|
|
5068
|
+
violations.push({
|
|
5069
|
+
rule: 'string-replace-all-occurrences',
|
|
5070
|
+
severity: 'low',
|
|
5071
|
+
line: path.node.loc?.start.line || 0,
|
|
5072
|
+
column: path.node.loc?.start.column || 0,
|
|
5073
|
+
message: `Note: replace() only replaces the first occurrence of '${searchValue}'. If you need to replace all occurrences, use replaceAll() or a global regex.`,
|
|
5074
|
+
suggestion: {
|
|
5075
|
+
text: `Consider if you need replaceAll() instead`,
|
|
5076
|
+
example: `str.replaceAll('${searchValue}', value) or str.replace(/${searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/g, value)`
|
|
5077
|
+
}
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
// Handle regex patterns - only warn if not global
|
|
5082
|
+
else if (t.isRegExpLiteral(searchArg)) {
|
|
5083
|
+
const flags = searchArg.flags || '';
|
|
5084
|
+
if (!flags.includes('g')) {
|
|
5085
|
+
violations.push({
|
|
5086
|
+
rule: 'string-replace-all-occurrences',
|
|
5087
|
+
severity: 'low',
|
|
5088
|
+
line: path.node.loc?.start.line || 0,
|
|
5089
|
+
column: path.node.loc?.start.column || 0,
|
|
5090
|
+
message: `Regex pattern without 'g' flag only replaces first match. Add 'g' flag for global replacement.`,
|
|
5091
|
+
suggestion: {
|
|
5092
|
+
text: `Add 'g' flag to replace all matches`,
|
|
5093
|
+
example: `str.replace(/${searchArg.pattern}/${flags}g, value)`
|
|
5094
|
+
}
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
});
|
|
5102
|
+
return violations;
|
|
5103
|
+
}
|
|
5104
|
+
},
|
|
4922
5105
|
{
|
|
4923
5106
|
name: 'component-props-validation',
|
|
4924
5107
|
appliesTo: 'all',
|
|
@@ -4927,8 +5110,10 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4927
5110
|
const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
|
|
4928
5111
|
// React special props that are automatically provided by React
|
|
4929
5112
|
const reactSpecialProps = new Set(['children']);
|
|
4930
|
-
// Build set of allowed props: standard props + React special props + componentSpec properties
|
|
5113
|
+
// Build set of allowed props: standard props + React special props + componentSpec properties + events
|
|
4931
5114
|
const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
|
|
5115
|
+
// Track required props separately for validation
|
|
5116
|
+
const requiredProps = new Set();
|
|
4932
5117
|
// Add props from componentSpec.properties if they exist
|
|
4933
5118
|
// These are the architect-defined props that this component is allowed to accept
|
|
4934
5119
|
const specDefinedProps = [];
|
|
@@ -4937,6 +5122,21 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4937
5122
|
if (prop.name) {
|
|
4938
5123
|
allowedProps.add(prop.name);
|
|
4939
5124
|
specDefinedProps.push(prop.name);
|
|
5125
|
+
if (prop.required) {
|
|
5126
|
+
requiredProps.add(prop.name);
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
// Add events from componentSpec.events if they exist
|
|
5132
|
+
// Events are functions passed as props to the component
|
|
5133
|
+
const specDefinedEvents = [];
|
|
5134
|
+
if (componentSpec?.events) {
|
|
5135
|
+
for (const event of componentSpec.events) {
|
|
5136
|
+
if (event.name) {
|
|
5137
|
+
allowedProps.add(event.name);
|
|
5138
|
+
specDefinedEvents.push(event.name);
|
|
5139
|
+
// Events are typically optional unless explicitly marked required
|
|
4940
5140
|
}
|
|
4941
5141
|
}
|
|
4942
5142
|
}
|
|
@@ -4956,23 +5156,36 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4956
5156
|
}
|
|
4957
5157
|
}
|
|
4958
5158
|
}
|
|
5159
|
+
// Check for missing required props
|
|
5160
|
+
const missingRequired = Array.from(requiredProps).filter(prop => !allProps.includes(prop) && !standardProps.has(prop));
|
|
5161
|
+
// Report missing required props
|
|
5162
|
+
if (missingRequired.length > 0) {
|
|
5163
|
+
violations.push({
|
|
5164
|
+
rule: 'component-props-validation',
|
|
5165
|
+
severity: 'critical',
|
|
5166
|
+
line: path.node.loc?.start.line || 0,
|
|
5167
|
+
column: path.node.loc?.start.column || 0,
|
|
5168
|
+
message: `Component "${componentName}" is missing required props: ${missingRequired.join(', ')}. These props are marked as required in the component specification.`
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
4959
5171
|
// Only report if there are non-allowed props
|
|
4960
5172
|
if (invalidProps.length > 0) {
|
|
4961
5173
|
let message;
|
|
4962
|
-
if (specDefinedProps.length > 0) {
|
|
5174
|
+
if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
|
|
4963
5175
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
4964
5176
|
`This component can only accept: ` +
|
|
4965
5177
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
4966
|
-
`(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
|
|
4967
|
-
`(3)
|
|
4968
|
-
`
|
|
5178
|
+
(specDefinedProps.length > 0 ? `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` : '') +
|
|
5179
|
+
(specDefinedEvents.length > 0 ? `(3) Spec-defined events: ${specDefinedEvents.join(', ')}, ` : '') +
|
|
5180
|
+
`(4) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5181
|
+
`Any additional props must be defined in the component spec's properties or events array.`;
|
|
4969
5182
|
}
|
|
4970
5183
|
else {
|
|
4971
5184
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
4972
5185
|
`This component can only accept: ` +
|
|
4973
5186
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
4974
5187
|
`(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
4975
|
-
`To accept additional props, they must be defined in the component spec's properties array.`;
|
|
5188
|
+
`To accept additional props, they must be defined in the component spec's properties or events array.`;
|
|
4976
5189
|
}
|
|
4977
5190
|
violations.push({
|
|
4978
5191
|
rule: 'component-props-validation',
|
|
@@ -5003,28 +5216,41 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5003
5216
|
}
|
|
5004
5217
|
}
|
|
5005
5218
|
}
|
|
5219
|
+
// Check for missing required props
|
|
5220
|
+
const missingRequired = Array.from(requiredProps).filter(prop => !allProps.includes(prop) && !standardProps.has(prop));
|
|
5221
|
+
// Report missing required props
|
|
5222
|
+
if (missingRequired.length > 0) {
|
|
5223
|
+
violations.push({
|
|
5224
|
+
rule: 'component-props-validation',
|
|
5225
|
+
severity: 'critical',
|
|
5226
|
+
line: init.loc?.start.line || 0,
|
|
5227
|
+
column: init.loc?.start.column || 0,
|
|
5228
|
+
message: `Component "${componentName}" is missing required props: ${missingRequired.join(', ')}. These props are marked as required in the component specification.`
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5006
5231
|
if (invalidProps.length > 0) {
|
|
5007
5232
|
let message;
|
|
5008
|
-
if (specDefinedProps.length > 0) {
|
|
5233
|
+
if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
|
|
5009
5234
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5010
5235
|
`This component can only accept: ` +
|
|
5011
5236
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5012
|
-
`(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
|
|
5013
|
-
`(3)
|
|
5014
|
-
`
|
|
5237
|
+
(specDefinedProps.length > 0 ? `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` : '') +
|
|
5238
|
+
(specDefinedEvents.length > 0 ? `(3) Spec-defined events: ${specDefinedEvents.join(', ')}, ` : '') +
|
|
5239
|
+
`(4) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5240
|
+
`Any additional props must be defined in the component spec's properties or events array.`;
|
|
5015
5241
|
}
|
|
5016
5242
|
else {
|
|
5017
5243
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5018
5244
|
`This component can only accept: ` +
|
|
5019
5245
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5020
5246
|
`(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5021
|
-
`To accept additional props, they must be defined in the component spec's properties array.`;
|
|
5247
|
+
`To accept additional props, they must be defined in the component spec's properties or events array.`;
|
|
5022
5248
|
}
|
|
5023
5249
|
violations.push({
|
|
5024
5250
|
rule: 'component-props-validation',
|
|
5025
5251
|
severity: 'critical',
|
|
5026
|
-
line:
|
|
5027
|
-
column:
|
|
5252
|
+
line: init.loc?.start.line || 0,
|
|
5253
|
+
column: init.loc?.start.column || 0,
|
|
5028
5254
|
message
|
|
5029
5255
|
});
|
|
5030
5256
|
}
|
|
@@ -5625,7 +5851,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5625
5851
|
severity: 'critical',
|
|
5626
5852
|
line: openingElement.loc?.start.line || 0,
|
|
5627
5853
|
column: openingElement.loc?.start.column || 0,
|
|
5628
|
-
message: `JSX component "${tagName}" is not defined. This looks like it should be
|
|
5854
|
+
message: `JSX component "${tagName}" is not defined. This looks like it should be from the ${libraryNames[0]} library. Add: const { ${tagName} } = unwrapComponents(${libraryNames[0]}, ['${tagName}']); at the top of your component function.`,
|
|
5629
5855
|
code: `<${tagName} ... />`
|
|
5630
5856
|
});
|
|
5631
5857
|
} else {
|
|
@@ -5635,7 +5861,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5635
5861
|
severity: 'critical',
|
|
5636
5862
|
line: openingElement.loc?.start.line || 0,
|
|
5637
5863
|
column: openingElement.loc?.start.column || 0,
|
|
5638
|
-
message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}.
|
|
5864
|
+
message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}. Use unwrapComponents to access it: const { ${tagName} } = unwrapComponents(LibraryName, ['${tagName}']);`,
|
|
5639
5865
|
code: `<${tagName} ... />`
|
|
5640
5866
|
});
|
|
5641
5867
|
}
|
|
@@ -5668,7 +5894,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5668
5894
|
severity: 'high',
|
|
5669
5895
|
line: openingElement.loc?.start.line || 0,
|
|
5670
5896
|
column: openingElement.loc?.start.column || 0,
|
|
5671
|
-
message: `JSX component "${tagName}" is not defined.
|
|
5897
|
+
message: `JSX component "${tagName}" is not defined. You must either: (1) define it in your component, (2) use a component that's already in the spec's dependencies, or (3) destructure it from a library that's already in the spec's libraries.`,
|
|
5672
5898
|
code: `<${tagName} ... />`
|
|
5673
5899
|
});
|
|
5674
5900
|
}
|
|
@@ -5696,42 +5922,13 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5696
5922
|
appliesTo: 'all',
|
|
5697
5923
|
test: (ast, componentName, componentSpec) => {
|
|
5698
5924
|
const violations = [];
|
|
5699
|
-
//
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
if (componentSpec?.dataRequirements) {
|
|
5703
|
-
// Handle queries in different possible locations
|
|
5704
|
-
if (Array.isArray(componentSpec.dataRequirements)) {
|
|
5705
|
-
// If it's an array directly
|
|
5706
|
-
componentSpec.dataRequirements.forEach((req) => {
|
|
5707
|
-
if (req.type === 'query' && req.name) {
|
|
5708
|
-
declaredQueries.add(req.name.toLowerCase());
|
|
5709
|
-
}
|
|
5710
|
-
if (req.type === 'entity' && req.name) {
|
|
5711
|
-
declaredEntities.add(req.name.toLowerCase());
|
|
5712
|
-
}
|
|
5713
|
-
});
|
|
5714
|
-
}
|
|
5715
|
-
else if (typeof componentSpec.dataRequirements === 'object') {
|
|
5716
|
-
// If it's an object with queries/entities properties
|
|
5717
|
-
if (componentSpec.dataRequirements.queries) {
|
|
5718
|
-
componentSpec.dataRequirements.queries.forEach((q) => {
|
|
5719
|
-
if (q.name)
|
|
5720
|
-
declaredQueries.add(q.name.toLowerCase());
|
|
5721
|
-
});
|
|
5722
|
-
}
|
|
5723
|
-
if (componentSpec.dataRequirements.entities) {
|
|
5724
|
-
componentSpec.dataRequirements.entities.forEach((e) => {
|
|
5725
|
-
if (e.name)
|
|
5726
|
-
declaredEntities.add(e.name.toLowerCase());
|
|
5727
|
-
});
|
|
5728
|
-
}
|
|
5729
|
-
}
|
|
5730
|
-
}
|
|
5925
|
+
// NOTE: Entity/Query name validation removed from this rule to avoid duplication
|
|
5926
|
+
// The 'data-requirements-validation' rule handles comprehensive entity/query validation
|
|
5927
|
+
// This rule now focuses on RunQuery/RunView specific issues like SQL injection
|
|
5731
5928
|
(0, traverse_1.default)(ast, {
|
|
5732
5929
|
CallExpression(path) {
|
|
5733
5930
|
const callee = path.node.callee;
|
|
5734
|
-
// Check for RunQuery calls
|
|
5931
|
+
// Check for RunQuery calls - focus on SQL injection detection
|
|
5735
5932
|
if (t.isMemberExpression(callee) &&
|
|
5736
5933
|
t.isIdentifier(callee.property) &&
|
|
5737
5934
|
callee.property.name === 'RunQuery') {
|
|
@@ -5761,20 +5958,9 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5761
5958
|
code: value.value.substring(0, 100)
|
|
5762
5959
|
});
|
|
5763
5960
|
}
|
|
5764
|
-
else if (declaredQueries.size > 0 && !declaredQueries.has(queryName.toLowerCase())) {
|
|
5765
|
-
// Only validate if we have declared queries
|
|
5766
|
-
violations.push({
|
|
5767
|
-
rule: 'runquery-runview-validation',
|
|
5768
|
-
severity: 'high',
|
|
5769
|
-
line: value.loc?.start.line || 0,
|
|
5770
|
-
column: value.loc?.start.column || 0,
|
|
5771
|
-
message: `Query "${queryName}" is not declared in dataRequirements.queries. Available queries: ${Array.from(declaredQueries).join(', ')}`,
|
|
5772
|
-
code: path.toString().substring(0, 100)
|
|
5773
|
-
});
|
|
5774
|
-
}
|
|
5775
5961
|
}
|
|
5776
5962
|
else if (t.isIdentifier(value) || t.isTemplateLiteral(value)) {
|
|
5777
|
-
// Dynamic query name -
|
|
5963
|
+
// Dynamic query name - warn that it shouldn't be SQL
|
|
5778
5964
|
violations.push({
|
|
5779
5965
|
rule: 'runquery-runview-validation',
|
|
5780
5966
|
severity: 'medium',
|
|
@@ -5787,43 +5973,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5787
5973
|
}
|
|
5788
5974
|
}
|
|
5789
5975
|
}
|
|
5790
|
-
//
|
|
5791
|
-
if (t.isMemberExpression(callee) &&
|
|
5792
|
-
t.isIdentifier(callee.property) &&
|
|
5793
|
-
(callee.property.name === 'RunView' || callee.property.name === 'RunViews')) {
|
|
5794
|
-
const args = path.node.arguments;
|
|
5795
|
-
// Handle both single object and array of objects
|
|
5796
|
-
const checkEntityName = (objExpr) => {
|
|
5797
|
-
const entityNameProp = objExpr.properties.find(p => t.isObjectProperty(p) &&
|
|
5798
|
-
t.isIdentifier(p.key) &&
|
|
5799
|
-
p.key.name === 'EntityName');
|
|
5800
|
-
if (entityNameProp && t.isObjectProperty(entityNameProp) && t.isStringLiteral(entityNameProp.value)) {
|
|
5801
|
-
const entityName = entityNameProp.value.value;
|
|
5802
|
-
if (declaredEntities.size > 0 && !declaredEntities.has(entityName.toLowerCase())) {
|
|
5803
|
-
violations.push({
|
|
5804
|
-
rule: 'runquery-runview-validation',
|
|
5805
|
-
severity: 'high',
|
|
5806
|
-
line: entityNameProp.value.loc?.start.line || 0,
|
|
5807
|
-
column: entityNameProp.value.loc?.start.column || 0,
|
|
5808
|
-
message: `Entity "${entityName}" is not declared in dataRequirements.entities. Available entities: ${Array.from(declaredEntities).join(', ')}`,
|
|
5809
|
-
code: path.toString().substring(0, 100)
|
|
5810
|
-
});
|
|
5811
|
-
}
|
|
5812
|
-
}
|
|
5813
|
-
};
|
|
5814
|
-
if (args.length > 0) {
|
|
5815
|
-
if (t.isObjectExpression(args[0])) {
|
|
5816
|
-
checkEntityName(args[0]);
|
|
5817
|
-
}
|
|
5818
|
-
else if (t.isArrayExpression(args[0])) {
|
|
5819
|
-
args[0].elements.forEach(elem => {
|
|
5820
|
-
if (t.isObjectExpression(elem)) {
|
|
5821
|
-
checkEntityName(elem);
|
|
5822
|
-
}
|
|
5823
|
-
});
|
|
5824
|
-
}
|
|
5825
|
-
}
|
|
5826
|
-
}
|
|
5976
|
+
// RunView validation removed - handled by data-requirements-validation
|
|
5827
5977
|
}
|
|
5828
5978
|
});
|
|
5829
5979
|
return violations;
|
|
@@ -8053,6 +8203,263 @@ const [state, setState] = useState(initialValue);`
|
|
|
8053
8203
|
});
|
|
8054
8204
|
return violations;
|
|
8055
8205
|
}
|
|
8206
|
+
},
|
|
8207
|
+
{
|
|
8208
|
+
name: 'callbacks-usage-validation',
|
|
8209
|
+
appliesTo: 'all',
|
|
8210
|
+
test: (ast, componentName, componentSpec) => {
|
|
8211
|
+
const violations = [];
|
|
8212
|
+
// Define the allowed methods on ComponentCallbacks interface
|
|
8213
|
+
const allowedCallbackMethods = new Set(['OpenEntityRecord', 'RegisterMethod']);
|
|
8214
|
+
// Build list of component's event names from spec
|
|
8215
|
+
const componentEvents = new Set();
|
|
8216
|
+
if (componentSpec?.events) {
|
|
8217
|
+
for (const event of componentSpec.events) {
|
|
8218
|
+
if (event.name) {
|
|
8219
|
+
componentEvents.add(event.name);
|
|
8220
|
+
}
|
|
8221
|
+
}
|
|
8222
|
+
}
|
|
8223
|
+
(0, traverse_1.default)(ast, {
|
|
8224
|
+
MemberExpression(path) {
|
|
8225
|
+
// Check for callbacks.something access
|
|
8226
|
+
if (t.isIdentifier(path.node.object) && path.node.object.name === 'callbacks') {
|
|
8227
|
+
if (t.isIdentifier(path.node.property)) {
|
|
8228
|
+
const methodName = path.node.property.name;
|
|
8229
|
+
// Check if it's trying to access an event
|
|
8230
|
+
if (componentEvents.has(methodName)) {
|
|
8231
|
+
violations.push({
|
|
8232
|
+
rule: 'callbacks-usage-validation',
|
|
8233
|
+
severity: 'critical',
|
|
8234
|
+
line: path.node.loc?.start.line || 0,
|
|
8235
|
+
column: path.node.loc?.start.column || 0,
|
|
8236
|
+
message: `Event "${methodName}" should not be accessed from callbacks. Events are passed as direct props to the component. Use the prop directly: ${methodName}`,
|
|
8237
|
+
suggestion: {
|
|
8238
|
+
text: `Events defined in the component spec are passed as direct props, not through callbacks. Access the event directly as a prop.`,
|
|
8239
|
+
example: `// ❌ WRONG - Accessing event from callbacks
|
|
8240
|
+
const { ${methodName} } = callbacks || {};
|
|
8241
|
+
callbacks?.${methodName}?.(data);
|
|
8242
|
+
|
|
8243
|
+
// ✅ CORRECT - Event is a direct prop
|
|
8244
|
+
// In the component props destructuring:
|
|
8245
|
+
function MyComponent({ ..., ${methodName} }) {
|
|
8246
|
+
// Use with null checking:
|
|
8247
|
+
if (${methodName}) {
|
|
8248
|
+
${methodName}(data);
|
|
8249
|
+
}
|
|
8250
|
+
}`
|
|
8251
|
+
}
|
|
8252
|
+
});
|
|
8253
|
+
}
|
|
8254
|
+
else if (!allowedCallbackMethods.has(methodName)) {
|
|
8255
|
+
// It's not an allowed callback method
|
|
8256
|
+
violations.push({
|
|
8257
|
+
rule: 'callbacks-usage-validation',
|
|
8258
|
+
severity: 'critical',
|
|
8259
|
+
line: path.node.loc?.start.line || 0,
|
|
8260
|
+
column: path.node.loc?.start.column || 0,
|
|
8261
|
+
message: `Invalid callback method "${methodName}". The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
|
|
8262
|
+
suggestion: {
|
|
8263
|
+
text: `The callbacks prop is reserved for specific MemberJunction framework methods. Custom events should be defined in the component spec's events array and passed as props.`,
|
|
8264
|
+
example: `// Allowed callbacks methods:
|
|
8265
|
+
callbacks?.OpenEntityRecord?.(entityName, key);
|
|
8266
|
+
callbacks?.RegisterMethod?.(methodName, handler);
|
|
8267
|
+
|
|
8268
|
+
// For custom events, define them in the spec and use as props:
|
|
8269
|
+
function MyComponent({ onCustomEvent }) {
|
|
8270
|
+
if (onCustomEvent) {
|
|
8271
|
+
onCustomEvent(data);
|
|
8272
|
+
}
|
|
8273
|
+
}`
|
|
8274
|
+
}
|
|
8275
|
+
});
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
},
|
|
8280
|
+
// Also check for destructuring from callbacks
|
|
8281
|
+
VariableDeclarator(path) {
|
|
8282
|
+
if (t.isObjectPattern(path.node.id) &&
|
|
8283
|
+
t.isIdentifier(path.node.init) &&
|
|
8284
|
+
path.node.init.name === 'callbacks') {
|
|
8285
|
+
// Check each destructured property
|
|
8286
|
+
for (const prop of path.node.id.properties) {
|
|
8287
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
8288
|
+
const methodName = prop.key.name;
|
|
8289
|
+
if (componentEvents.has(methodName)) {
|
|
8290
|
+
violations.push({
|
|
8291
|
+
rule: 'callbacks-usage-validation',
|
|
8292
|
+
severity: 'critical',
|
|
8293
|
+
line: prop.loc?.start.line || 0,
|
|
8294
|
+
column: prop.loc?.start.column || 0,
|
|
8295
|
+
message: `Event "${methodName}" should not be destructured from callbacks. Events are passed as direct props to the component.`,
|
|
8296
|
+
suggestion: {
|
|
8297
|
+
text: `Events should be destructured from the component props, not from callbacks.`,
|
|
8298
|
+
example: `// ❌ WRONG
|
|
8299
|
+
const { ${methodName} } = callbacks || {};
|
|
8300
|
+
|
|
8301
|
+
// ✅ CORRECT
|
|
8302
|
+
function MyComponent({ utilities, styles, callbacks, ${methodName} }) {
|
|
8303
|
+
// ${methodName} is now available as a prop
|
|
8304
|
+
}`
|
|
8305
|
+
}
|
|
8306
|
+
});
|
|
8307
|
+
}
|
|
8308
|
+
else if (!allowedCallbackMethods.has(methodName)) {
|
|
8309
|
+
violations.push({
|
|
8310
|
+
rule: 'callbacks-usage-validation',
|
|
8311
|
+
severity: 'critical',
|
|
8312
|
+
line: prop.loc?.start.line || 0,
|
|
8313
|
+
column: prop.loc?.start.column || 0,
|
|
8314
|
+
message: `Invalid callback method "${methodName}" being destructured. The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
|
|
8315
|
+
});
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
// Also check for: const { something } = callbacks || {}
|
|
8321
|
+
if (t.isObjectPattern(path.node.id) &&
|
|
8322
|
+
t.isLogicalExpression(path.node.init) &&
|
|
8323
|
+
path.node.init.operator === '||' &&
|
|
8324
|
+
t.isIdentifier(path.node.init.left) &&
|
|
8325
|
+
path.node.init.left.name === 'callbacks') {
|
|
8326
|
+
// Check each destructured property
|
|
8327
|
+
for (const prop of path.node.id.properties) {
|
|
8328
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
8329
|
+
const methodName = prop.key.name;
|
|
8330
|
+
if (componentEvents.has(methodName)) {
|
|
8331
|
+
violations.push({
|
|
8332
|
+
rule: 'callbacks-usage-validation',
|
|
8333
|
+
severity: 'critical',
|
|
8334
|
+
line: prop.loc?.start.line || 0,
|
|
8335
|
+
column: prop.loc?.start.column || 0,
|
|
8336
|
+
message: `Event "${methodName}" should not be destructured from callbacks. Events are passed as direct props to the component.`,
|
|
8337
|
+
suggestion: {
|
|
8338
|
+
text: `Events should be destructured from the component props, not from callbacks.`,
|
|
8339
|
+
example: `// ❌ WRONG
|
|
8340
|
+
const { ${methodName} } = callbacks || {};
|
|
8341
|
+
|
|
8342
|
+
// ✅ CORRECT
|
|
8343
|
+
function MyComponent({ utilities, styles, callbacks, ${methodName} }) {
|
|
8344
|
+
// ${methodName} is now available as a prop
|
|
8345
|
+
}`
|
|
8346
|
+
}
|
|
8347
|
+
});
|
|
8348
|
+
}
|
|
8349
|
+
else if (!allowedCallbackMethods.has(methodName)) {
|
|
8350
|
+
violations.push({
|
|
8351
|
+
rule: 'callbacks-usage-validation',
|
|
8352
|
+
severity: 'critical',
|
|
8353
|
+
line: prop.loc?.start.line || 0,
|
|
8354
|
+
column: prop.loc?.start.column || 0,
|
|
8355
|
+
message: `Invalid callback method "${methodName}" being destructured. The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
|
|
8356
|
+
});
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
});
|
|
8363
|
+
return violations;
|
|
8364
|
+
}
|
|
8365
|
+
},
|
|
8366
|
+
{
|
|
8367
|
+
name: 'event-invocation-pattern',
|
|
8368
|
+
appliesTo: 'all',
|
|
8369
|
+
test: (ast, componentName, componentSpec) => {
|
|
8370
|
+
const violations = [];
|
|
8371
|
+
// Build list of component's event names from spec
|
|
8372
|
+
const componentEvents = new Set();
|
|
8373
|
+
if (componentSpec?.events) {
|
|
8374
|
+
for (const event of componentSpec.events) {
|
|
8375
|
+
if (event.name) {
|
|
8376
|
+
componentEvents.add(event.name);
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
// If no events defined, skip this rule
|
|
8381
|
+
if (componentEvents.size === 0) {
|
|
8382
|
+
return violations;
|
|
8383
|
+
}
|
|
8384
|
+
(0, traverse_1.default)(ast, {
|
|
8385
|
+
CallExpression(path) {
|
|
8386
|
+
// Check if calling an event without null checking
|
|
8387
|
+
if (t.isIdentifier(path.node.callee)) {
|
|
8388
|
+
const eventName = path.node.callee.name;
|
|
8389
|
+
if (componentEvents.has(eventName)) {
|
|
8390
|
+
// Check if this call is inside a conditional that checks for the event
|
|
8391
|
+
let hasNullCheck = false;
|
|
8392
|
+
let currentPath = path.parentPath;
|
|
8393
|
+
// Walk up the tree to see if we're inside an if statement that checks this event
|
|
8394
|
+
while (currentPath && !hasNullCheck) {
|
|
8395
|
+
if (t.isIfStatement(currentPath.node)) {
|
|
8396
|
+
const test = currentPath.node.test;
|
|
8397
|
+
// Check if the test checks for the event (simple cases)
|
|
8398
|
+
if (t.isIdentifier(test) && test.name === eventName) {
|
|
8399
|
+
hasNullCheck = true;
|
|
8400
|
+
}
|
|
8401
|
+
else if (t.isLogicalExpression(test) && test.operator === '&&') {
|
|
8402
|
+
// Check for patterns like: eventName && ...
|
|
8403
|
+
if (t.isIdentifier(test.left) && test.left.name === eventName) {
|
|
8404
|
+
hasNullCheck = true;
|
|
8405
|
+
}
|
|
8406
|
+
}
|
|
8407
|
+
}
|
|
8408
|
+
else if (t.isLogicalExpression(currentPath.node) && currentPath.node.operator === '&&') {
|
|
8409
|
+
// Check for inline conditional: eventName && eventName()
|
|
8410
|
+
if (t.isIdentifier(currentPath.node.left) && currentPath.node.left.name === eventName) {
|
|
8411
|
+
hasNullCheck = true;
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
else if (t.isConditionalExpression(currentPath.node)) {
|
|
8415
|
+
// Check for ternary: eventName ? eventName() : null
|
|
8416
|
+
if (t.isIdentifier(currentPath.node.test) && currentPath.node.test.name === eventName) {
|
|
8417
|
+
hasNullCheck = true;
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
8420
|
+
currentPath = currentPath.parentPath || null;
|
|
8421
|
+
}
|
|
8422
|
+
if (!hasNullCheck) {
|
|
8423
|
+
violations.push({
|
|
8424
|
+
rule: 'event-invocation-pattern',
|
|
8425
|
+
severity: 'medium',
|
|
8426
|
+
line: path.node.loc?.start.line || 0,
|
|
8427
|
+
column: path.node.loc?.start.column || 0,
|
|
8428
|
+
message: `Event "${eventName}" is being invoked without null-checking. Events are optional props and should be checked before invocation.`,
|
|
8429
|
+
suggestion: {
|
|
8430
|
+
text: `Always check that an event prop exists before invoking it, as events are optional.`,
|
|
8431
|
+
example: `// ❌ WRONG - No null check
|
|
8432
|
+
${eventName}(data);
|
|
8433
|
+
|
|
8434
|
+
// ✅ CORRECT - With null check
|
|
8435
|
+
if (${eventName}) {
|
|
8436
|
+
${eventName}(data);
|
|
8437
|
+
}
|
|
8438
|
+
|
|
8439
|
+
// ✅ ALSO CORRECT - Inline check
|
|
8440
|
+
${eventName} && ${eventName}(data);
|
|
8441
|
+
|
|
8442
|
+
// ✅ ALSO CORRECT - Optional chaining
|
|
8443
|
+
${eventName}?.(data);`
|
|
8444
|
+
}
|
|
8445
|
+
});
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
}
|
|
8449
|
+
},
|
|
8450
|
+
// Check for optional chaining on events (this is good!)
|
|
8451
|
+
OptionalCallExpression(path) {
|
|
8452
|
+
if (t.isIdentifier(path.node.callee)) {
|
|
8453
|
+
const eventName = path.node.callee.name;
|
|
8454
|
+
if (componentEvents.has(eventName)) {
|
|
8455
|
+
// This is actually the correct pattern, no violation
|
|
8456
|
+
return;
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8460
|
+
});
|
|
8461
|
+
return violations;
|
|
8462
|
+
}
|
|
8056
8463
|
}
|
|
8057
8464
|
];
|
|
8058
8465
|
//# sourceMappingURL=component-linter.js.map
|