@memberjunction/react-test-harness 2.89.0 → 2.91.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/README.md +98 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/browser-context.d.ts.map +1 -1
- package/dist/lib/browser-context.js +6 -2
- package/dist/lib/browser-context.js.map +1 -1
- package/dist/lib/component-linter.d.ts +1 -0
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +2620 -663
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +19 -49
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +925 -745
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/test-harness.d.ts +32 -0
- package/dist/lib/test-harness.d.ts.map +1 -1
- package/dist/lib/test-harness.js +52 -0
- package/dist/lib/test-harness.js.map +1 -1
- package/package.json +3 -3
|
@@ -30,11 +30,111 @@ 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
|
+
// Standard HTML elements (lowercase)
|
|
34
|
+
const HTML_ELEMENTS = new Set([
|
|
35
|
+
// Main root
|
|
36
|
+
'html',
|
|
37
|
+
// Document metadata
|
|
38
|
+
'base', 'head', 'link', 'meta', 'style', 'title',
|
|
39
|
+
// Sectioning root
|
|
40
|
+
'body',
|
|
41
|
+
// Content sectioning
|
|
42
|
+
'address', 'article', 'aside', 'footer', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
43
|
+
'main', 'nav', 'section',
|
|
44
|
+
// Text content
|
|
45
|
+
'blockquote', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure', 'hr', 'li', 'menu', 'ol', 'p', 'pre', 'ul',
|
|
46
|
+
// Inline text semantics
|
|
47
|
+
'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd', 'mark',
|
|
48
|
+
'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',
|
|
49
|
+
// Image and multimedia
|
|
50
|
+
'area', 'audio', 'img', 'map', 'track', 'video',
|
|
51
|
+
// Embedded content
|
|
52
|
+
'embed', 'iframe', 'object', 'param', 'picture', 'portal', 'source',
|
|
53
|
+
// SVG and MathML
|
|
54
|
+
'svg', 'math',
|
|
55
|
+
// Scripting
|
|
56
|
+
'canvas', 'noscript', 'script',
|
|
57
|
+
// Demarcating edits
|
|
58
|
+
'del', 'ins',
|
|
59
|
+
// Table content
|
|
60
|
+
'caption', 'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr',
|
|
61
|
+
// Forms
|
|
62
|
+
'button', 'datalist', 'fieldset', 'form', 'input', 'label', 'legend', 'meter', 'optgroup',
|
|
63
|
+
'option', 'output', 'progress', 'select', 'textarea',
|
|
64
|
+
// Interactive elements
|
|
65
|
+
'details', 'dialog', 'summary',
|
|
66
|
+
// Web Components
|
|
67
|
+
'slot', 'template',
|
|
68
|
+
// SVG elements (common ones)
|
|
69
|
+
'animate', 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'defs', 'desc', 'ellipse',
|
|
70
|
+
'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix',
|
|
71
|
+
'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood',
|
|
72
|
+
'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode',
|
|
73
|
+
'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile',
|
|
74
|
+
'feTurbulence', 'filter', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker',
|
|
75
|
+
'mask', 'metadata', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect',
|
|
76
|
+
'stop', 'switch', 'symbol', 'text', 'textPath', 'tspan', 'use', 'view'
|
|
77
|
+
]);
|
|
78
|
+
// React built-in components (PascalCase)
|
|
79
|
+
const REACT_BUILT_INS = new Set([
|
|
80
|
+
'Fragment',
|
|
81
|
+
'StrictMode',
|
|
82
|
+
'Suspense',
|
|
83
|
+
'Profiler'
|
|
84
|
+
]);
|
|
33
85
|
// Helper function
|
|
34
86
|
function getLineNumber(code, index) {
|
|
35
87
|
return code.substring(0, index).split('\n').length;
|
|
36
88
|
}
|
|
89
|
+
// Extract property names from TypeScript types at compile time
|
|
90
|
+
// These will be evaluated at TypeScript compile time and become static arrays
|
|
91
|
+
const runQueryResultProps = [
|
|
92
|
+
'QueryID', 'QueryName', 'Success', 'Results', 'RowCount',
|
|
93
|
+
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
94
|
+
];
|
|
95
|
+
const runViewResultProps = [
|
|
96
|
+
'Success', 'Results', 'UserViewRunID', 'RowCount',
|
|
97
|
+
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
98
|
+
];
|
|
37
99
|
class ComponentLinter {
|
|
100
|
+
// Helper method to check if a variable comes from RunQuery or RunView
|
|
101
|
+
static isVariableFromRunQueryOrView(path, varName, methodName) {
|
|
102
|
+
let isFromMethod = false;
|
|
103
|
+
// Look up the binding for this variable
|
|
104
|
+
const binding = path.scope.getBinding(varName);
|
|
105
|
+
if (binding && binding.path) {
|
|
106
|
+
// Check if it's from a .then() or await of RunQuery/RunView
|
|
107
|
+
const parent = binding.path.parent;
|
|
108
|
+
if (t.isVariableDeclarator(binding.path.node)) {
|
|
109
|
+
const init = binding.path.node.init;
|
|
110
|
+
// Check for await utilities.rq.RunQuery or utilities.rv.RunView
|
|
111
|
+
if (t.isAwaitExpression(init) && t.isCallExpression(init.argument)) {
|
|
112
|
+
const callee = init.argument.callee;
|
|
113
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
114
|
+
if (callee.property.name === methodName ||
|
|
115
|
+
callee.property.name === methodName + 's') { // RunViews
|
|
116
|
+
isFromMethod = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Check for .then() pattern
|
|
121
|
+
if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
|
|
122
|
+
if (t.isIdentifier(init.callee.property) && init.callee.property.name === 'then') {
|
|
123
|
+
// Check if the object being called is RunQuery/RunView
|
|
124
|
+
const obj = init.callee.object;
|
|
125
|
+
if (t.isCallExpression(obj) && t.isMemberExpression(obj.callee)) {
|
|
126
|
+
if (t.isIdentifier(obj.callee.property) &&
|
|
127
|
+
(obj.callee.property.name === methodName ||
|
|
128
|
+
obj.callee.property.name === methodName + 's')) {
|
|
129
|
+
isFromMethod = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return isFromMethod;
|
|
137
|
+
}
|
|
38
138
|
static async lintComponent(code, componentName, componentSpec, isRootComponent) {
|
|
39
139
|
try {
|
|
40
140
|
const ast = parser.parse(code, {
|
|
@@ -103,6 +203,8 @@ class ComponentLinter {
|
|
|
103
203
|
// Extract entity names from dataRequirements
|
|
104
204
|
const requiredEntities = new Set();
|
|
105
205
|
const requiredQueries = new Set();
|
|
206
|
+
// Map to store full query definitions for parameter validation
|
|
207
|
+
const queryDefinitionsMap = new Map();
|
|
106
208
|
// Map to track allowed fields per entity
|
|
107
209
|
const entityFieldsMap = new Map();
|
|
108
210
|
if (componentSpec.dataRequirements?.entities) {
|
|
@@ -121,6 +223,7 @@ class ComponentLinter {
|
|
|
121
223
|
for (const query of componentSpec.dataRequirements.queries) {
|
|
122
224
|
if (query.name) {
|
|
123
225
|
requiredQueries.add(query.name);
|
|
226
|
+
queryDefinitionsMap.set(query.name, query);
|
|
124
227
|
}
|
|
125
228
|
}
|
|
126
229
|
}
|
|
@@ -152,6 +255,7 @@ class ComponentLinter {
|
|
|
152
255
|
for (const query of dep.dataRequirements.queries) {
|
|
153
256
|
if (query.name) {
|
|
154
257
|
requiredQueries.add(query.name);
|
|
258
|
+
queryDefinitionsMap.set(query.name, query);
|
|
155
259
|
}
|
|
156
260
|
}
|
|
157
261
|
}
|
|
@@ -189,17 +293,38 @@ class ComponentLinter {
|
|
|
189
293
|
const usedEntity = prop.value.value;
|
|
190
294
|
// Check if this entity is in the required entities
|
|
191
295
|
if (requiredEntities.size > 0 && !requiredEntities.has(usedEntity)) {
|
|
192
|
-
//
|
|
193
|
-
const possibleMatches = Array.from(requiredEntities).filter(e =>
|
|
194
|
-
|
|
296
|
+
// Enhanced fuzzy matching for better suggestions
|
|
297
|
+
const possibleMatches = Array.from(requiredEntities).filter(e => {
|
|
298
|
+
const eLower = e.toLowerCase();
|
|
299
|
+
const usedLower = usedEntity.toLowerCase();
|
|
300
|
+
// Check various matching patterns
|
|
301
|
+
return (
|
|
302
|
+
// Contains match
|
|
303
|
+
eLower.includes(usedLower) ||
|
|
304
|
+
usedLower.includes(eLower) ||
|
|
305
|
+
// Remove spaces and check
|
|
306
|
+
eLower.replace(/\s+/g, '').includes(usedLower.replace(/\s+/g, '')) ||
|
|
307
|
+
usedLower.replace(/\s+/g, '').includes(eLower.replace(/\s+/g, '')) ||
|
|
308
|
+
// Check if the main words match (ignore prefixes like "MJ:")
|
|
309
|
+
eLower.replace(/^mj:\s*/i, '').includes(usedLower) ||
|
|
310
|
+
usedLower.includes(eLower.replace(/^mj:\s*/i, '')));
|
|
311
|
+
});
|
|
312
|
+
// Always show all available entities for clarity
|
|
313
|
+
const allEntities = Array.from(requiredEntities);
|
|
314
|
+
const entityList = allEntities.length <= 5
|
|
315
|
+
? allEntities.join(', ')
|
|
316
|
+
: allEntities.slice(0, 5).join(', ') + `, ... (${allEntities.length} total)`;
|
|
317
|
+
let message = `Entity "${usedEntity}" not found in dataRequirements.`;
|
|
318
|
+
if (possibleMatches.length > 0) {
|
|
319
|
+
message += ` Did you mean "${possibleMatches[0]}"?`;
|
|
320
|
+
}
|
|
321
|
+
message += ` Available entities: ${entityList}`;
|
|
195
322
|
violations.push({
|
|
196
323
|
rule: 'entity-name-mismatch',
|
|
197
324
|
severity: 'critical',
|
|
198
325
|
line: prop.value.loc?.start.line || 0,
|
|
199
326
|
column: prop.value.loc?.start.column || 0,
|
|
200
|
-
message
|
|
201
|
-
? `Did you mean "${possibleMatches[0]}"?`
|
|
202
|
-
: `Available entities: ${Array.from(requiredEntities).join(', ')}`}`,
|
|
327
|
+
message,
|
|
203
328
|
code: `EntityName: "${usedEntity}"`
|
|
204
329
|
});
|
|
205
330
|
}
|
|
@@ -288,22 +413,104 @@ class ComponentLinter {
|
|
|
288
413
|
const usedQuery = prop.value.value;
|
|
289
414
|
// Check if this query is in the required queries
|
|
290
415
|
if (requiredQueries.size > 0 && !requiredQueries.has(usedQuery)) {
|
|
291
|
-
//
|
|
292
|
-
const possibleMatches = Array.from(requiredQueries).filter(q =>
|
|
293
|
-
|
|
416
|
+
// Enhanced fuzzy matching for better suggestions
|
|
417
|
+
const possibleMatches = Array.from(requiredQueries).filter(q => {
|
|
418
|
+
const qLower = q.toLowerCase();
|
|
419
|
+
const usedLower = usedQuery.toLowerCase();
|
|
420
|
+
return (
|
|
421
|
+
// Contains match
|
|
422
|
+
qLower.includes(usedLower) ||
|
|
423
|
+
usedLower.includes(qLower) ||
|
|
424
|
+
// Remove spaces and check
|
|
425
|
+
qLower.replace(/\s+/g, '').includes(usedLower.replace(/\s+/g, '')) ||
|
|
426
|
+
usedLower.replace(/\s+/g, '').includes(qLower.replace(/\s+/g, '')));
|
|
427
|
+
});
|
|
428
|
+
// Always show all available queries for clarity
|
|
429
|
+
const allQueries = Array.from(requiredQueries);
|
|
430
|
+
const queryList = allQueries.length <= 5
|
|
431
|
+
? allQueries.join(', ')
|
|
432
|
+
: allQueries.slice(0, 5).join(', ') + `, ... (${allQueries.length} total)`;
|
|
433
|
+
let message = `Query "${usedQuery}" not found in dataRequirements.`;
|
|
434
|
+
if (possibleMatches.length > 0) {
|
|
435
|
+
message += ` Did you mean "${possibleMatches[0]}"?`;
|
|
436
|
+
}
|
|
437
|
+
if (requiredQueries.size > 0) {
|
|
438
|
+
message += ` Available queries: ${queryList}`;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
message += ` No queries defined in dataRequirements.`;
|
|
442
|
+
}
|
|
294
443
|
violations.push({
|
|
295
444
|
rule: 'query-name-mismatch',
|
|
296
445
|
severity: 'critical',
|
|
297
446
|
line: prop.value.loc?.start.line || 0,
|
|
298
447
|
column: prop.value.loc?.start.column || 0,
|
|
299
|
-
message
|
|
300
|
-
? `Did you mean "${possibleMatches[0]}"?`
|
|
301
|
-
: requiredQueries.size > 0
|
|
302
|
-
? `Available queries: ${Array.from(requiredQueries).join(', ')}`
|
|
303
|
-
: `No queries defined in dataRequirements`}`,
|
|
448
|
+
message,
|
|
304
449
|
code: `QueryName: "${usedQuery}"`
|
|
305
450
|
});
|
|
306
451
|
}
|
|
452
|
+
else if (queryDefinitionsMap.has(usedQuery)) {
|
|
453
|
+
// Query is valid, now check parameters
|
|
454
|
+
const queryDef = queryDefinitionsMap.get(usedQuery);
|
|
455
|
+
if (queryDef?.parameters && queryDef.parameters.length > 0) {
|
|
456
|
+
// Extract parameters from the RunQuery call
|
|
457
|
+
const paramsInCall = new Map();
|
|
458
|
+
// Look for Parameters property in the config object
|
|
459
|
+
for (const prop of configObj.properties) {
|
|
460
|
+
if (t.isObjectProperty(prop) &&
|
|
461
|
+
t.isIdentifier(prop.key) &&
|
|
462
|
+
prop.key.name === 'Parameters' &&
|
|
463
|
+
t.isObjectExpression(prop.value)) {
|
|
464
|
+
// Extract each parameter from the Parameters object
|
|
465
|
+
for (const paramProp of prop.value.properties) {
|
|
466
|
+
if (t.isObjectProperty(paramProp) && t.isIdentifier(paramProp.key)) {
|
|
467
|
+
paramsInCall.set(paramProp.key.name, paramProp);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Check for required parameters
|
|
471
|
+
const requiredParams = queryDef.parameters.filter(p => p.value !== '@runtime' || p.value === '@runtime');
|
|
472
|
+
for (const reqParam of requiredParams) {
|
|
473
|
+
if (!paramsInCall.has(reqParam.name)) {
|
|
474
|
+
violations.push({
|
|
475
|
+
rule: 'missing-query-parameter',
|
|
476
|
+
severity: 'critical',
|
|
477
|
+
line: prop.value.loc?.start.line || 0,
|
|
478
|
+
column: prop.value.loc?.start.column || 0,
|
|
479
|
+
message: `Missing required parameter "${reqParam.name}" for query "${usedQuery}". ${reqParam.description ? `Description: ${reqParam.description}` : ''}`,
|
|
480
|
+
code: `Parameters: { ${reqParam.name}: ... }`
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Check for unknown parameters
|
|
485
|
+
const validParamNames = new Set(queryDef.parameters.map(p => p.name));
|
|
486
|
+
for (const [paramName, paramNode] of paramsInCall) {
|
|
487
|
+
if (!validParamNames.has(paramName)) {
|
|
488
|
+
violations.push({
|
|
489
|
+
rule: 'unknown-query-parameter',
|
|
490
|
+
severity: 'high',
|
|
491
|
+
line: paramNode.loc?.start.line || 0,
|
|
492
|
+
column: paramNode.loc?.start.column || 0,
|
|
493
|
+
message: `Unknown parameter "${paramName}" for query "${usedQuery}". Valid parameters: ${Array.from(validParamNames).join(', ')}`,
|
|
494
|
+
code: `${paramName}: ...`
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
break; // Found Parameters property, no need to continue
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// If query has parameters but no Parameters property was found in the call
|
|
502
|
+
if (paramsInCall.size === 0 && queryDef.parameters.length > 0) {
|
|
503
|
+
violations.push({
|
|
504
|
+
rule: 'missing-parameters-object',
|
|
505
|
+
severity: 'critical',
|
|
506
|
+
line: configObj.loc?.start.line || 0,
|
|
507
|
+
column: configObj.loc?.start.column || 0,
|
|
508
|
+
message: `Query "${usedQuery}" requires parameters but none were provided. Required parameters: ${queryDef.parameters.map(p => p.name).join(', ')}`,
|
|
509
|
+
code: `RunQuery({ QueryName: "${usedQuery}", Parameters: { ... } })`
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
307
514
|
}
|
|
308
515
|
}
|
|
309
516
|
}
|
|
@@ -359,6 +566,211 @@ class ComponentLinter {
|
|
|
359
566
|
const suggestions = [];
|
|
360
567
|
for (const violation of violations) {
|
|
361
568
|
switch (violation.rule) {
|
|
569
|
+
case 'no-import-statements':
|
|
570
|
+
suggestions.push({
|
|
571
|
+
violation: violation.rule,
|
|
572
|
+
suggestion: 'Remove all import statements. Interactive components receive everything through props.',
|
|
573
|
+
example: `// ❌ WRONG - Using import statements:
|
|
574
|
+
import React from 'react';
|
|
575
|
+
import { useState } from 'react';
|
|
576
|
+
import { format } from 'date-fns';
|
|
577
|
+
import './styles.css';
|
|
578
|
+
|
|
579
|
+
function MyComponent({ utilities, styles }) {
|
|
580
|
+
// ...
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ✅ CORRECT - Everything passed as props:
|
|
584
|
+
function MyComponent({ utilities, styles, components }) {
|
|
585
|
+
// React hooks are available globally (useState, useEffect, etc.)
|
|
586
|
+
const [value, setValue] = useState('');
|
|
587
|
+
|
|
588
|
+
// Utilities include formatting functions
|
|
589
|
+
const formatted = utilities.formatDate(new Date());
|
|
590
|
+
|
|
591
|
+
// Styles are passed as props
|
|
592
|
+
return <div style={styles.container}>...</div>;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// All dependencies must be:
|
|
596
|
+
// 1. Passed through the 'utilities' prop (formatting, helpers)
|
|
597
|
+
// 2. Passed through the 'components' prop (child components)
|
|
598
|
+
// 3. Passed through the 'styles' prop (styling)
|
|
599
|
+
// 4. Available globally (React hooks)`
|
|
600
|
+
});
|
|
601
|
+
break;
|
|
602
|
+
case 'no-export-statements':
|
|
603
|
+
suggestions.push({
|
|
604
|
+
violation: violation.rule,
|
|
605
|
+
suggestion: 'Remove all export statements. The component function should be the only code, not exported.',
|
|
606
|
+
example: `// ❌ WRONG - Using export:
|
|
607
|
+
export function MyComponent({ utilities }) {
|
|
608
|
+
return <div>Hello</div>;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export const helper = () => {};
|
|
612
|
+
export default MyComponent;
|
|
613
|
+
|
|
614
|
+
// ✅ CORRECT - Just the function, no exports:
|
|
615
|
+
function MyComponent({ utilities, styles, components }) {
|
|
616
|
+
// Helper functions defined inside if needed
|
|
617
|
+
const helper = () => {
|
|
618
|
+
// ...
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
return <div>Hello</div>;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// The component is self-contained.
|
|
625
|
+
// No exports needed - the host environment
|
|
626
|
+
// will execute the function directly.`
|
|
627
|
+
});
|
|
628
|
+
break;
|
|
629
|
+
case 'no-require-statements':
|
|
630
|
+
suggestions.push({
|
|
631
|
+
violation: violation.rule,
|
|
632
|
+
suggestion: 'Remove all require() and dynamic import() statements. Use props instead.',
|
|
633
|
+
example: `// ❌ WRONG - Using require or dynamic import:
|
|
634
|
+
function MyComponent({ utilities }) {
|
|
635
|
+
const lodash = require('lodash');
|
|
636
|
+
const module = await import('./module');
|
|
637
|
+
|
|
638
|
+
return <div>...</div>;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ✅ CORRECT - Use utilities and components props:
|
|
642
|
+
function MyComponent({ utilities, styles, components }) {
|
|
643
|
+
// Use utilities for helper functions
|
|
644
|
+
const result = utilities.debounce(() => {
|
|
645
|
+
// ...
|
|
646
|
+
}, 300);
|
|
647
|
+
|
|
648
|
+
// Use components prop for child components
|
|
649
|
+
const { DataTable, FilterPanel } = components;
|
|
650
|
+
|
|
651
|
+
return (
|
|
652
|
+
<div>
|
|
653
|
+
<DataTable {...props} />
|
|
654
|
+
<FilterPanel {...props} />
|
|
655
|
+
</div>
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Everything the component needs must be:
|
|
660
|
+
// - Passed via props (utilities, components, styles)
|
|
661
|
+
// - Available globally (React hooks)
|
|
662
|
+
// No module loading allowed!`
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
665
|
+
case 'use-function-declaration':
|
|
666
|
+
suggestions.push({
|
|
667
|
+
violation: violation.rule,
|
|
668
|
+
suggestion: 'Use function declaration syntax for TOP-LEVEL component definitions. Arrow functions are fine inside components.',
|
|
669
|
+
example: `// ❌ WRONG - Top-level arrow function component:
|
|
670
|
+
const MyComponent = ({ utilities, styles, components }) => {
|
|
671
|
+
const [state, setState] = useState('');
|
|
672
|
+
|
|
673
|
+
return <div>{state}</div>;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// ✅ CORRECT - Function declaration for top-level:
|
|
677
|
+
function MyComponent({ utilities, styles, components }) {
|
|
678
|
+
const [state, setState] = useState('');
|
|
679
|
+
|
|
680
|
+
// Arrow functions are FINE inside the component:
|
|
681
|
+
const handleClick = () => {
|
|
682
|
+
setState('clicked');
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const ChildComponent = () => <div>This is OK inside the component</div>;
|
|
686
|
+
|
|
687
|
+
return <div onClick={handleClick}>{state}</div>;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Child components also use function declaration:
|
|
691
|
+
function ChildComponent() {
|
|
692
|
+
return <div>Child</div>;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Why function declarations?
|
|
696
|
+
// 1. Clearer component identification
|
|
697
|
+
// 2. Better debugging experience (named functions)
|
|
698
|
+
// 3. Hoisting allows flexible code organization
|
|
699
|
+
// 4. Consistent with React documentation patterns
|
|
700
|
+
// 5. Easier to distinguish from regular variables`
|
|
701
|
+
});
|
|
702
|
+
break;
|
|
703
|
+
case 'no-return-component':
|
|
704
|
+
suggestions.push({
|
|
705
|
+
violation: violation.rule,
|
|
706
|
+
suggestion: 'Remove the return statement at the end of the file. The component function should stand alone.',
|
|
707
|
+
example: `// ❌ WRONG - Returning the component:
|
|
708
|
+
function MyComponent({ utilities, styles, components }) {
|
|
709
|
+
const [state, setState] = useState('');
|
|
710
|
+
|
|
711
|
+
return <div>{state}</div>;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return MyComponent; // <-- Remove this!
|
|
715
|
+
|
|
716
|
+
// ❌ ALSO WRONG - Component reference at end:
|
|
717
|
+
function MyComponent({ utilities, styles, components }) {
|
|
718
|
+
return <div>Hello</div>;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
MyComponent; // <-- Remove this!
|
|
722
|
+
|
|
723
|
+
// ✅ CORRECT - Just the function declaration:
|
|
724
|
+
function MyComponent({ utilities, styles, components }) {
|
|
725
|
+
const [state, setState] = useState('');
|
|
726
|
+
|
|
727
|
+
return <div>{state}</div>;
|
|
728
|
+
}
|
|
729
|
+
// Nothing after the function - file ends here
|
|
730
|
+
|
|
731
|
+
// The runtime will find and execute your component
|
|
732
|
+
// by its function name. No need to return or reference it!`
|
|
733
|
+
});
|
|
734
|
+
break;
|
|
735
|
+
case 'no-iife-wrapper':
|
|
736
|
+
suggestions.push({
|
|
737
|
+
violation: violation.rule,
|
|
738
|
+
suggestion: 'Remove the IIFE wrapper. Component code should be plain functions, not wrapped in immediately invoked functions.',
|
|
739
|
+
example: `// ❌ WRONG - IIFE wrapper patterns:
|
|
740
|
+
(function() {
|
|
741
|
+
function MyComponent({ utilities, styles, components }) {
|
|
742
|
+
return <div>Hello</div>;
|
|
743
|
+
}
|
|
744
|
+
return MyComponent;
|
|
745
|
+
})();
|
|
746
|
+
|
|
747
|
+
// Also wrong:
|
|
748
|
+
(function() {
|
|
749
|
+
const MyComponent = ({ utilities }) => {
|
|
750
|
+
return <div>Hello</div>;
|
|
751
|
+
};
|
|
752
|
+
})();
|
|
753
|
+
|
|
754
|
+
// Also wrong - arrow function IIFE:
|
|
755
|
+
(() => {
|
|
756
|
+
function MyComponent({ utilities }) {
|
|
757
|
+
return <div>Hello</div>;
|
|
758
|
+
}
|
|
759
|
+
})();
|
|
760
|
+
|
|
761
|
+
// ✅ CORRECT - Direct function declaration:
|
|
762
|
+
function MyComponent({ utilities, styles, components }) {
|
|
763
|
+
return <div>Hello</div>;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Why no IIFE?
|
|
767
|
+
// 1. Components run in their own scope already
|
|
768
|
+
// 2. The runtime handles isolation
|
|
769
|
+
// 3. IIFEs prevent proper component discovery
|
|
770
|
+
// 4. Makes debugging harder
|
|
771
|
+
// 5. Unnecessary complexity`
|
|
772
|
+
});
|
|
773
|
+
break;
|
|
362
774
|
case 'full-state-ownership':
|
|
363
775
|
suggestions.push({
|
|
364
776
|
violation: violation.rule,
|
|
@@ -634,6 +1046,86 @@ await utilities.rv.RunViews([
|
|
|
634
1046
|
// match those declared in the component spec's dataRequirements`
|
|
635
1047
|
});
|
|
636
1048
|
break;
|
|
1049
|
+
case 'missing-query-parameter':
|
|
1050
|
+
suggestions.push({
|
|
1051
|
+
violation: violation.rule,
|
|
1052
|
+
suggestion: 'Provide all required parameters defined in dataRequirements for the query',
|
|
1053
|
+
example: `// The component spec defines required parameters:
|
|
1054
|
+
// dataRequirements: {
|
|
1055
|
+
// queries: [
|
|
1056
|
+
// {
|
|
1057
|
+
// name: "User Activity Summary",
|
|
1058
|
+
// parameters: [
|
|
1059
|
+
// { name: "UserID", value: "@runtime", description: "User to filter by" },
|
|
1060
|
+
// { name: "StartDate", value: "@runtime", description: "Start of date range" }
|
|
1061
|
+
// ]
|
|
1062
|
+
// }
|
|
1063
|
+
// ]
|
|
1064
|
+
// }
|
|
1065
|
+
|
|
1066
|
+
// ❌ WRONG - Missing required parameter:
|
|
1067
|
+
await utilities.rq.RunQuery({
|
|
1068
|
+
QueryName: "User Activity Summary",
|
|
1069
|
+
Parameters: {
|
|
1070
|
+
UserID: currentUserId
|
|
1071
|
+
// Missing StartDate!
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
// ✅ CORRECT - All required parameters provided:
|
|
1076
|
+
await utilities.rq.RunQuery({
|
|
1077
|
+
QueryName: "User Activity Summary",
|
|
1078
|
+
Parameters: {
|
|
1079
|
+
UserID: currentUserId,
|
|
1080
|
+
StartDate: startDate // All parameters included
|
|
1081
|
+
}
|
|
1082
|
+
});`
|
|
1083
|
+
});
|
|
1084
|
+
break;
|
|
1085
|
+
case 'unknown-query-parameter':
|
|
1086
|
+
suggestions.push({
|
|
1087
|
+
violation: violation.rule,
|
|
1088
|
+
suggestion: 'Only use parameters that are defined in dataRequirements for the query',
|
|
1089
|
+
example: `// ❌ WRONG - Using undefined parameter:
|
|
1090
|
+
await utilities.rq.RunQuery({
|
|
1091
|
+
QueryName: "User Activity Summary",
|
|
1092
|
+
Parameters: {
|
|
1093
|
+
UserID: currentUserId,
|
|
1094
|
+
EndDate: endDate, // Not defined in dataRequirements!
|
|
1095
|
+
ExtraParam: 123 // Unknown parameter!
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// ✅ CORRECT - Only use defined parameters:
|
|
1100
|
+
await utilities.rq.RunQuery({
|
|
1101
|
+
QueryName: "User Activity Summary",
|
|
1102
|
+
Parameters: {
|
|
1103
|
+
UserID: currentUserId,
|
|
1104
|
+
StartDate: startDate // Only parameters from dataRequirements
|
|
1105
|
+
}
|
|
1106
|
+
});`
|
|
1107
|
+
});
|
|
1108
|
+
break;
|
|
1109
|
+
case 'missing-parameters-object':
|
|
1110
|
+
suggestions.push({
|
|
1111
|
+
violation: violation.rule,
|
|
1112
|
+
suggestion: 'Queries with parameters must include a Parameters object in RunQuery',
|
|
1113
|
+
example: `// ❌ WRONG - Query requires parameters but none provided:
|
|
1114
|
+
await utilities.rq.RunQuery({
|
|
1115
|
+
QueryName: "User Activity Summary"
|
|
1116
|
+
// Missing Parameters object!
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// ✅ CORRECT - Include Parameters object:
|
|
1120
|
+
await utilities.rq.RunQuery({
|
|
1121
|
+
QueryName: "User Activity Summary",
|
|
1122
|
+
Parameters: {
|
|
1123
|
+
UserID: currentUserId,
|
|
1124
|
+
StartDate: startDate
|
|
1125
|
+
}
|
|
1126
|
+
});`
|
|
1127
|
+
});
|
|
1128
|
+
break;
|
|
637
1129
|
case 'query-name-mismatch':
|
|
638
1130
|
suggestions.push({
|
|
639
1131
|
violation: violation.rule,
|
|
@@ -995,6 +1487,57 @@ function RootComponent({ utilities, styles, components, callbacks, savedUserSett
|
|
|
995
1487
|
}`
|
|
996
1488
|
});
|
|
997
1489
|
break;
|
|
1490
|
+
case 'runview-runquery-result-direct-usage':
|
|
1491
|
+
suggestions.push({
|
|
1492
|
+
violation: violation.rule,
|
|
1493
|
+
suggestion: 'RunView and RunQuery return result objects, not arrays. Access the data with .Results property.',
|
|
1494
|
+
example: `// ❌ WRONG - Using result directly as array:
|
|
1495
|
+
const result = await utilities.rv.RunView({
|
|
1496
|
+
EntityName: 'Users',
|
|
1497
|
+
Fields: ['ID', 'Name']
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
// These will all fail:
|
|
1501
|
+
setUsers(result); // Wrong! result is an object
|
|
1502
|
+
result.map(u => u.Name); // Wrong! Can't map on object
|
|
1503
|
+
const users = Array.isArray(result) ? result : []; // Wrong! Will always be []
|
|
1504
|
+
|
|
1505
|
+
// ✅ CORRECT - Access the Results property:
|
|
1506
|
+
const result = await utilities.rv.RunView({
|
|
1507
|
+
EntityName: 'Users',
|
|
1508
|
+
Fields: ['ID', 'Name']
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
// Check success first (recommended):
|
|
1512
|
+
if (result.Success) {
|
|
1513
|
+
setUsers(result.Results || []);
|
|
1514
|
+
} else {
|
|
1515
|
+
console.error('Failed:', result.ErrorMessage);
|
|
1516
|
+
setUsers([]);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Or use optional chaining:
|
|
1520
|
+
setUsers(result?.Results || []);
|
|
1521
|
+
|
|
1522
|
+
// Now array methods work:
|
|
1523
|
+
const names = result.Results?.map(u => u.Name) || [];
|
|
1524
|
+
|
|
1525
|
+
// ✅ For RunQuery - same pattern:
|
|
1526
|
+
const queryResult = await utilities.rq.RunQuery({
|
|
1527
|
+
QueryName: 'UserSummary'
|
|
1528
|
+
});
|
|
1529
|
+
setData(queryResult.Results || []); // NOT queryResult directly!
|
|
1530
|
+
|
|
1531
|
+
// Result object structure:
|
|
1532
|
+
// {
|
|
1533
|
+
// Success: boolean,
|
|
1534
|
+
// Results: Array, // Your data is here!
|
|
1535
|
+
// ErrorMessage?: string,
|
|
1536
|
+
// TotalRowCount?: number,
|
|
1537
|
+
// ExecutionTime?: number
|
|
1538
|
+
// }`
|
|
1539
|
+
});
|
|
1540
|
+
break;
|
|
998
1541
|
}
|
|
999
1542
|
}
|
|
1000
1543
|
return suggestions;
|
|
@@ -1004,118 +1547,94 @@ exports.ComponentLinter = ComponentLinter;
|
|
|
1004
1547
|
// Universal rules that apply to all components with SavedUserSettings pattern
|
|
1005
1548
|
ComponentLinter.universalComponentRules = [
|
|
1006
1549
|
{
|
|
1007
|
-
name: 'no-
|
|
1550
|
+
name: 'no-import-statements',
|
|
1008
1551
|
appliesTo: 'all',
|
|
1009
1552
|
test: (ast, componentName, componentSpec) => {
|
|
1010
1553
|
const violations = [];
|
|
1011
1554
|
(0, traverse_1.default)(ast, {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
line: path.node.loc?.start.line || 0,
|
|
1022
|
-
column: path.node.loc?.start.column || 0,
|
|
1023
|
-
message: `Component "${componentName}" uses useReducer at line ${path.node.loc?.start.line}. Components should manage state with useState and persist important settings with onSaveUserSettings.`,
|
|
1024
|
-
code: path.toString()
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1555
|
+
ImportDeclaration(path) {
|
|
1556
|
+
violations.push({
|
|
1557
|
+
rule: 'no-import-statements',
|
|
1558
|
+
severity: 'critical',
|
|
1559
|
+
line: path.node.loc?.start.line || 0,
|
|
1560
|
+
column: path.node.loc?.start.column || 0,
|
|
1561
|
+
message: `Component "${componentName}" contains an import statement. Interactive components cannot use import statements - all dependencies must be passed as props.`,
|
|
1562
|
+
code: path.toString().substring(0, 100)
|
|
1563
|
+
});
|
|
1027
1564
|
}
|
|
1028
1565
|
});
|
|
1029
1566
|
return violations;
|
|
1030
1567
|
}
|
|
1031
1568
|
},
|
|
1032
|
-
// New rules for the controlled component pattern
|
|
1033
1569
|
{
|
|
1034
|
-
name: 'no-
|
|
1570
|
+
name: 'no-export-statements',
|
|
1035
1571
|
appliesTo: 'all',
|
|
1036
1572
|
test: (ast, componentName, componentSpec) => {
|
|
1037
1573
|
const violations = [];
|
|
1038
1574
|
(0, traverse_1.default)(ast, {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
severity: 'medium', // It's a pattern issue, not critical
|
|
1049
|
-
line: prop.loc?.start.line || 0,
|
|
1050
|
-
column: prop.loc?.start.column || 0,
|
|
1051
|
-
message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
|
|
1052
|
-
code: 'data prop in component signature'
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1575
|
+
ExportNamedDeclaration(path) {
|
|
1576
|
+
violations.push({
|
|
1577
|
+
rule: 'no-export-statements',
|
|
1578
|
+
severity: 'critical',
|
|
1579
|
+
line: path.node.loc?.start.line || 0,
|
|
1580
|
+
column: path.node.loc?.start.column || 0,
|
|
1581
|
+
message: `Component "${componentName}" contains an export statement. Interactive components are self-contained and cannot export values.`,
|
|
1582
|
+
code: path.toString().substring(0, 100)
|
|
1583
|
+
});
|
|
1058
1584
|
},
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1585
|
+
ExportDefaultDeclaration(path) {
|
|
1586
|
+
violations.push({
|
|
1587
|
+
rule: 'no-export-statements',
|
|
1588
|
+
severity: 'critical',
|
|
1589
|
+
line: path.node.loc?.start.line || 0,
|
|
1590
|
+
column: path.node.loc?.start.column || 0,
|
|
1591
|
+
message: `Component "${componentName}" contains an export default statement. Interactive components are self-contained and cannot export values.`,
|
|
1592
|
+
code: path.toString().substring(0, 100)
|
|
1593
|
+
});
|
|
1594
|
+
},
|
|
1595
|
+
ExportAllDeclaration(path) {
|
|
1596
|
+
violations.push({
|
|
1597
|
+
rule: 'no-export-statements',
|
|
1598
|
+
severity: 'critical',
|
|
1599
|
+
line: path.node.loc?.start.line || 0,
|
|
1600
|
+
column: path.node.loc?.start.column || 0,
|
|
1601
|
+
message: `Component "${componentName}" contains an export * statement. Interactive components are self-contained and cannot export values.`,
|
|
1602
|
+
code: path.toString().substring(0, 100)
|
|
1603
|
+
});
|
|
1081
1604
|
}
|
|
1082
1605
|
});
|
|
1083
1606
|
return violations;
|
|
1084
1607
|
}
|
|
1085
1608
|
},
|
|
1086
1609
|
{
|
|
1087
|
-
name: '
|
|
1610
|
+
name: 'no-require-statements',
|
|
1088
1611
|
appliesTo: 'all',
|
|
1089
1612
|
test: (ast, componentName, componentSpec) => {
|
|
1090
1613
|
const violations = [];
|
|
1091
|
-
// Check for improper onSaveUserSettings usage
|
|
1092
1614
|
(0, traverse_1.default)(ast, {
|
|
1093
1615
|
CallExpression(path) {
|
|
1094
1616
|
const callee = path.node.callee;
|
|
1095
|
-
// Check for
|
|
1096
|
-
if (t.
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1617
|
+
// Check for require() calls
|
|
1618
|
+
if (t.isIdentifier(callee) && callee.name === 'require') {
|
|
1619
|
+
violations.push({
|
|
1620
|
+
rule: 'no-require-statements',
|
|
1621
|
+
severity: 'critical',
|
|
1622
|
+
line: path.node.loc?.start.line || 0,
|
|
1623
|
+
column: path.node.loc?.start.column || 0,
|
|
1624
|
+
message: `Component "${componentName}" contains a require() statement. Interactive components cannot use require - all dependencies must be passed as props.`,
|
|
1625
|
+
code: path.toString().substring(0, 100)
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
// Also check for dynamic import() calls
|
|
1629
|
+
if (t.isImport(callee)) {
|
|
1630
|
+
violations.push({
|
|
1631
|
+
rule: 'no-require-statements',
|
|
1632
|
+
severity: 'critical',
|
|
1633
|
+
line: path.node.loc?.start.line || 0,
|
|
1634
|
+
column: path.node.loc?.start.column || 0,
|
|
1635
|
+
message: `Component "${componentName}" contains a dynamic import() statement. Interactive components cannot use dynamic imports - all dependencies must be passed as props.`,
|
|
1636
|
+
code: path.toString().substring(0, 100)
|
|
1637
|
+
});
|
|
1119
1638
|
}
|
|
1120
1639
|
}
|
|
1121
1640
|
});
|
|
@@ -1123,65 +1642,47 @@ ComponentLinter.universalComponentRules = [
|
|
|
1123
1642
|
}
|
|
1124
1643
|
},
|
|
1125
1644
|
{
|
|
1126
|
-
name: '
|
|
1645
|
+
name: 'use-function-declaration',
|
|
1127
1646
|
appliesTo: 'all',
|
|
1128
1647
|
test: (ast, componentName, componentSpec) => {
|
|
1129
1648
|
const violations = [];
|
|
1130
|
-
const requiredProps = ['styles', 'utilities', 'components'];
|
|
1131
|
-
// Build a set of our component names from componentSpec dependencies
|
|
1132
|
-
const ourComponentNames = new Set();
|
|
1133
|
-
// Add components from dependencies array
|
|
1134
|
-
if (componentSpec?.dependencies) {
|
|
1135
|
-
for (const dep of componentSpec.dependencies) {
|
|
1136
|
-
if (dep.name) {
|
|
1137
|
-
ourComponentNames.add(dep.name);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
// Also find components destructured from the components prop in the code
|
|
1142
1649
|
(0, traverse_1.default)(ast, {
|
|
1143
1650
|
VariableDeclarator(path) {
|
|
1144
|
-
//
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
path.
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
ourComponentNames.add(prop.key.name);
|
|
1151
|
-
}
|
|
1152
|
-
// Also handle renaming: { ComponentA: RenamedComponent }
|
|
1153
|
-
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
1154
|
-
ourComponentNames.add(prop.value.name);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1651
|
+
// Only check TOP-LEVEL declarations (not nested inside functions)
|
|
1652
|
+
// This prevents flagging arrow functions inside the component
|
|
1653
|
+
const isTopLevel = path.getFunctionParent() === null ||
|
|
1654
|
+
path.scope.path.type === 'Program';
|
|
1655
|
+
if (!isTopLevel) {
|
|
1656
|
+
return; // Skip non-top-level declarations
|
|
1157
1657
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const openingElement = path.node.openingElement;
|
|
1164
|
-
// Only check if it's one of our components
|
|
1165
|
-
if (t.isJSXIdentifier(openingElement.name) &&
|
|
1166
|
-
ourComponentNames.has(openingElement.name.name)) {
|
|
1167
|
-
const componentBeingCalled = openingElement.name.name;
|
|
1168
|
-
const passedProps = new Set();
|
|
1169
|
-
// Collect all props being passed
|
|
1170
|
-
for (const attr of openingElement.attributes) {
|
|
1171
|
-
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
1172
|
-
passedProps.add(attr.name.name);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
// Check if required props are missing
|
|
1176
|
-
const missingProps = requiredProps.filter(prop => !passedProps.has(prop));
|
|
1177
|
-
if (missingProps.length > 0) {
|
|
1658
|
+
// Check if this is the main component being defined as arrow function
|
|
1659
|
+
if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
|
|
1660
|
+
const init = path.node.init;
|
|
1661
|
+
// Check if it's an arrow function
|
|
1662
|
+
if (t.isArrowFunctionExpression(init)) {
|
|
1178
1663
|
violations.push({
|
|
1179
|
-
rule: '
|
|
1664
|
+
rule: 'use-function-declaration',
|
|
1180
1665
|
severity: 'critical',
|
|
1181
|
-
line:
|
|
1182
|
-
column:
|
|
1183
|
-
message: `Component "${
|
|
1184
|
-
code:
|
|
1666
|
+
line: path.node.loc?.start.line || 0,
|
|
1667
|
+
column: path.node.loc?.start.column || 0,
|
|
1668
|
+
message: `Component "${componentName}" must be defined using function declaration syntax, not arrow function.`,
|
|
1669
|
+
code: path.toString().substring(0, 150)
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
// Also check for any other TOP-LEVEL component-like arrow functions (starts with capital letter)
|
|
1674
|
+
// But ONLY at the top level, not inside the component
|
|
1675
|
+
if (t.isIdentifier(path.node.id) && /^[A-Z]/.test(path.node.id.name)) {
|
|
1676
|
+
const init = path.node.init;
|
|
1677
|
+
if (t.isArrowFunctionExpression(init)) {
|
|
1678
|
+
// Only flag if it's at the top level (parallel to main component)
|
|
1679
|
+
violations.push({
|
|
1680
|
+
rule: 'use-function-declaration',
|
|
1681
|
+
severity: 'high',
|
|
1682
|
+
line: path.node.loc?.start.line || 0,
|
|
1683
|
+
column: path.node.loc?.start.column || 0,
|
|
1684
|
+
message: `Top-level component "${path.node.id.name}" should use function declaration syntax.`,
|
|
1685
|
+
code: path.toString().substring(0, 150)
|
|
1185
1686
|
});
|
|
1186
1687
|
}
|
|
1187
1688
|
}
|
|
@@ -1191,188 +1692,647 @@ ComponentLinter.universalComponentRules = [
|
|
|
1191
1692
|
}
|
|
1192
1693
|
},
|
|
1193
1694
|
{
|
|
1194
|
-
name: 'no-
|
|
1195
|
-
appliesTo: '
|
|
1695
|
+
name: 'no-return-component',
|
|
1696
|
+
appliesTo: 'all',
|
|
1196
1697
|
test: (ast, componentName, componentSpec) => {
|
|
1197
1698
|
const violations = [];
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1699
|
+
// Check for return statements at the program/top level
|
|
1700
|
+
if (ast.program && ast.program.body) {
|
|
1701
|
+
for (const statement of ast.program.body) {
|
|
1702
|
+
// Check for return statement returning the component
|
|
1703
|
+
if (t.isReturnStatement(statement)) {
|
|
1704
|
+
const argument = statement.argument;
|
|
1705
|
+
// Check if it's returning the component identifier or any identifier
|
|
1706
|
+
if (argument && t.isIdentifier(argument)) {
|
|
1707
|
+
// If it's returning the component name or any identifier at top level
|
|
1708
|
+
violations.push({
|
|
1709
|
+
rule: 'no-return-component',
|
|
1710
|
+
severity: 'critical',
|
|
1711
|
+
line: statement.loc?.start.line || 0,
|
|
1712
|
+
column: statement.loc?.start.column || 0,
|
|
1713
|
+
message: `Do not return the component at the end of the file. The component function should stand alone.`,
|
|
1714
|
+
code: `return ${argument.name};`
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
// Also check for expression statements that might be standalone identifiers
|
|
1719
|
+
if (t.isExpressionStatement(statement) &&
|
|
1720
|
+
t.isIdentifier(statement.expression) &&
|
|
1721
|
+
statement.expression.name === componentName) {
|
|
1722
|
+
violations.push({
|
|
1723
|
+
rule: 'no-return-component',
|
|
1724
|
+
severity: 'critical',
|
|
1725
|
+
line: statement.loc?.start.line || 0,
|
|
1726
|
+
column: statement.loc?.start.column || 0,
|
|
1727
|
+
message: `Do not reference the component "${componentName}" at the end of the file. The component function should stand alone.`,
|
|
1728
|
+
code: statement.expression.name
|
|
1729
|
+
});
|
|
1205
1730
|
}
|
|
1206
1731
|
}
|
|
1207
|
-
});
|
|
1208
|
-
// If there are multiple function declarations and they look like components
|
|
1209
|
-
// (start with capital letter), it's likely implementing children
|
|
1210
|
-
const componentFunctions = declaredFunctions.filter(name => name !== rootFunctionName && /^[A-Z]/.test(name));
|
|
1211
|
-
if (componentFunctions.length > 0) {
|
|
1212
|
-
violations.push({
|
|
1213
|
-
rule: 'no-child-implementation',
|
|
1214
|
-
severity: 'critical',
|
|
1215
|
-
line: 1,
|
|
1216
|
-
column: 0,
|
|
1217
|
-
message: `Root component file contains child component implementations: ${componentFunctions.join(', ')}. Root should only reference child components, not implement them.`,
|
|
1218
|
-
});
|
|
1219
1732
|
}
|
|
1220
1733
|
return violations;
|
|
1221
1734
|
}
|
|
1222
1735
|
},
|
|
1223
1736
|
{
|
|
1224
|
-
name: '
|
|
1737
|
+
name: 'no-window-access',
|
|
1225
1738
|
appliesTo: 'all',
|
|
1226
1739
|
test: (ast, componentName, componentSpec) => {
|
|
1227
1740
|
const violations = [];
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1741
|
+
// Build a map of library names to their global variables from the component spec
|
|
1742
|
+
const libraryMap = new Map();
|
|
1743
|
+
if (componentSpec?.libraries) {
|
|
1744
|
+
for (const lib of componentSpec.libraries) {
|
|
1745
|
+
if (lib.globalVariable) {
|
|
1746
|
+
// Store both the library name and globalVariable for lookup
|
|
1747
|
+
libraryMap.set(lib.name.toLowerCase(), lib.globalVariable);
|
|
1748
|
+
libraryMap.set(lib.globalVariable.toLowerCase(), lib.globalVariable);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1231
1752
|
(0, traverse_1.default)(ast, {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (t.
|
|
1235
|
-
// Check
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1753
|
+
MemberExpression(path) {
|
|
1754
|
+
// Check if accessing window object
|
|
1755
|
+
if (t.isIdentifier(path.node.object) && path.node.object.name === 'window') {
|
|
1756
|
+
// Check what property is being accessed from window
|
|
1757
|
+
let propertyName = '';
|
|
1758
|
+
let isDestructuring = false;
|
|
1759
|
+
if (t.isIdentifier(path.node.property)) {
|
|
1760
|
+
propertyName = path.node.property.name;
|
|
1761
|
+
}
|
|
1762
|
+
else if (t.isMemberExpression(path.node.property)) {
|
|
1763
|
+
// Handle chained access like window.Recharts.ResponsiveContainer
|
|
1764
|
+
const firstProp = path.node.property;
|
|
1765
|
+
if (t.isIdentifier(firstProp.object)) {
|
|
1766
|
+
propertyName = firstProp.object.name;
|
|
1242
1767
|
}
|
|
1243
1768
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
for (const prop of param.properties) {
|
|
1252
|
-
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'components') {
|
|
1253
|
-
hasComponentsProp = true;
|
|
1254
|
-
// Look for nested destructuring like { components: { A, B } }
|
|
1255
|
-
if (t.isObjectPattern(prop.value)) {
|
|
1256
|
-
for (const innerProp of prop.value.properties) {
|
|
1257
|
-
if (t.isObjectProperty(innerProp) && t.isIdentifier(innerProp.key)) {
|
|
1258
|
-
componentsFromProps.add(innerProp.key.name);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1769
|
+
// Check if this is part of a destructuring assignment
|
|
1770
|
+
let currentPath = path.parentPath;
|
|
1771
|
+
while (currentPath) {
|
|
1772
|
+
if (t.isVariableDeclarator(currentPath.node) &&
|
|
1773
|
+
t.isObjectPattern(currentPath.node.id)) {
|
|
1774
|
+
isDestructuring = true;
|
|
1775
|
+
break;
|
|
1263
1776
|
}
|
|
1777
|
+
currentPath = currentPath.parentPath;
|
|
1778
|
+
}
|
|
1779
|
+
// Check if the property matches a known library
|
|
1780
|
+
const matchedLibrary = libraryMap.get(propertyName.toLowerCase());
|
|
1781
|
+
if (matchedLibrary) {
|
|
1782
|
+
// Specific guidance for library access
|
|
1783
|
+
violations.push({
|
|
1784
|
+
rule: 'no-window-access',
|
|
1785
|
+
severity: 'critical',
|
|
1786
|
+
line: path.node.loc?.start.line || 0,
|
|
1787
|
+
column: path.node.loc?.start.column || 0,
|
|
1788
|
+
message: `Component "${componentName}" should not access window.${propertyName}. Use "${matchedLibrary}" directly - it's already available in the component's closure scope. Change "window.${propertyName}" to just "${matchedLibrary}".`,
|
|
1789
|
+
code: path.toString().substring(0, 100)
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
else if (isDestructuring) {
|
|
1793
|
+
// Likely trying to destructure from an unknown library
|
|
1794
|
+
violations.push({
|
|
1795
|
+
rule: 'no-window-access',
|
|
1796
|
+
severity: 'critical',
|
|
1797
|
+
line: path.node.loc?.start.line || 0,
|
|
1798
|
+
column: path.node.loc?.start.column || 0,
|
|
1799
|
+
message: `Component "${componentName}" is trying to destructure from window.${propertyName}. If this is a library, it should be added to the component's libraries array in the spec and accessed via its globalVariable name.`,
|
|
1800
|
+
code: path.toString().substring(0, 100)
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
// General window access
|
|
1805
|
+
violations.push({
|
|
1806
|
+
rule: 'no-window-access',
|
|
1807
|
+
severity: 'critical',
|
|
1808
|
+
line: path.node.loc?.start.line || 0,
|
|
1809
|
+
column: path.node.loc?.start.column || 0,
|
|
1810
|
+
message: `Component "${componentName}" must not access the window object. Interactive components should be self-contained and not rely on global state.`,
|
|
1811
|
+
code: path.toString().substring(0, 100)
|
|
1812
|
+
});
|
|
1264
1813
|
}
|
|
1265
1814
|
}
|
|
1266
1815
|
},
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1816
|
+
Identifier(path) {
|
|
1817
|
+
// Also check for direct window references (less common but possible)
|
|
1818
|
+
if (path.node.name === 'window' && path.isReferencedIdentifier()) {
|
|
1819
|
+
// Make sure it's not part of a member expression we already caught
|
|
1820
|
+
const parent = path.parent;
|
|
1821
|
+
if (!t.isMemberExpression(parent) || parent.object !== path.node) {
|
|
1822
|
+
violations.push({
|
|
1823
|
+
rule: 'no-window-access',
|
|
1824
|
+
severity: 'critical',
|
|
1825
|
+
line: path.node.loc?.start.line || 0,
|
|
1826
|
+
column: path.node.loc?.start.column || 0,
|
|
1827
|
+
message: `Component "${componentName}" must not reference the window object directly. Interactive components should be self-contained.`,
|
|
1828
|
+
code: path.toString().substring(0, 100)
|
|
1829
|
+
});
|
|
1275
1830
|
}
|
|
1276
1831
|
}
|
|
1277
1832
|
}
|
|
1278
1833
|
});
|
|
1279
|
-
// Only check if we found a components prop
|
|
1280
|
-
if (hasComponentsProp && componentsFromProps.size > 0) {
|
|
1281
|
-
// Find components that are destructured but never used
|
|
1282
|
-
const unusedComponents = Array.from(componentsFromProps).filter(comp => !componentsUsedInJSX.has(comp));
|
|
1283
|
-
if (unusedComponents.length > 0) {
|
|
1284
|
-
violations.push({
|
|
1285
|
-
rule: 'undefined-component-usage',
|
|
1286
|
-
severity: 'low',
|
|
1287
|
-
line: 1,
|
|
1288
|
-
column: 0,
|
|
1289
|
-
message: `Component destructures ${unusedComponents.join(', ')} from components prop but never uses them. These may be missing from the component spec's dependencies array.`
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
1834
|
return violations;
|
|
1294
1835
|
}
|
|
1295
1836
|
},
|
|
1296
1837
|
{
|
|
1297
|
-
name: '
|
|
1838
|
+
name: 'no-iife-wrapper',
|
|
1298
1839
|
appliesTo: 'all',
|
|
1299
1840
|
test: (ast, componentName, componentSpec) => {
|
|
1300
1841
|
const violations = [];
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
if (t.
|
|
1309
|
-
const
|
|
1310
|
-
// Check if
|
|
1311
|
-
if (
|
|
1842
|
+
// Check if the entire code is wrapped in an IIFE
|
|
1843
|
+
if (ast.program && ast.program.body) {
|
|
1844
|
+
for (const statement of ast.program.body) {
|
|
1845
|
+
// Check for IIFE pattern: (function() { ... })() or (function() { ... }())
|
|
1846
|
+
if (t.isExpressionStatement(statement)) {
|
|
1847
|
+
const expr = statement.expression;
|
|
1848
|
+
// Pattern 1: (function() { ... })()
|
|
1849
|
+
if (t.isCallExpression(expr)) {
|
|
1850
|
+
const callee = expr.callee;
|
|
1851
|
+
// Check if calling a function expression wrapped in parentheses
|
|
1852
|
+
if (t.isParenthesizedExpression && t.isParenthesizedExpression(callee)) {
|
|
1853
|
+
const inner = callee.expression;
|
|
1854
|
+
if (t.isFunctionExpression(inner) || t.isArrowFunctionExpression(inner)) {
|
|
1855
|
+
violations.push({
|
|
1856
|
+
rule: 'no-iife-wrapper',
|
|
1857
|
+
severity: 'critical',
|
|
1858
|
+
line: statement.loc?.start.line || 0,
|
|
1859
|
+
column: statement.loc?.start.column || 0,
|
|
1860
|
+
message: `Component code must not be wrapped in an IIFE (Immediately Invoked Function Expression). Define the component function directly.`,
|
|
1861
|
+
code: statement.toString().substring(0, 50) + '...'
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
// Also check without ParenthesizedExpression (some parsers handle it differently)
|
|
1866
|
+
if (t.isFunctionExpression(callee) || t.isArrowFunctionExpression(callee)) {
|
|
1312
1867
|
violations.push({
|
|
1313
|
-
rule: '
|
|
1868
|
+
rule: 'no-iife-wrapper',
|
|
1314
1869
|
severity: 'critical',
|
|
1315
|
-
line:
|
|
1316
|
-
column:
|
|
1317
|
-
message: `
|
|
1318
|
-
code:
|
|
1870
|
+
line: statement.loc?.start.line || 0,
|
|
1871
|
+
column: statement.loc?.start.column || 0,
|
|
1872
|
+
message: `Component code must not be wrapped in an IIFE. Define the component function directly.`,
|
|
1873
|
+
code: statement.toString().substring(0, 50) + '...'
|
|
1319
1874
|
});
|
|
1320
1875
|
}
|
|
1321
1876
|
}
|
|
1877
|
+
// Pattern 2: (function() { ... }())
|
|
1878
|
+
if (t.isParenthesizedExpression && t.isParenthesizedExpression(expr)) {
|
|
1879
|
+
const inner = expr.expression;
|
|
1880
|
+
if (t.isCallExpression(inner)) {
|
|
1881
|
+
const callee = inner.callee;
|
|
1882
|
+
if (t.isFunctionExpression(callee) || t.isArrowFunctionExpression(callee)) {
|
|
1883
|
+
violations.push({
|
|
1884
|
+
rule: 'no-iife-wrapper',
|
|
1885
|
+
severity: 'critical',
|
|
1886
|
+
line: statement.loc?.start.line || 0,
|
|
1887
|
+
column: statement.loc?.start.column || 0,
|
|
1888
|
+
message: `Component code must not be wrapped in an IIFE. Define the component function directly.`,
|
|
1889
|
+
code: statement.toString().substring(0, 50) + '...'
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
// Also check for variable assignment with IIFE
|
|
1896
|
+
if (t.isVariableDeclaration(statement)) {
|
|
1897
|
+
for (const decl of statement.declarations) {
|
|
1898
|
+
if (decl.init && t.isCallExpression(decl.init)) {
|
|
1899
|
+
const callee = decl.init.callee;
|
|
1900
|
+
if (t.isFunctionExpression(callee) || t.isArrowFunctionExpression(callee)) {
|
|
1901
|
+
violations.push({
|
|
1902
|
+
rule: 'no-iife-wrapper',
|
|
1903
|
+
severity: 'critical',
|
|
1904
|
+
line: decl.loc?.start.line || 0,
|
|
1905
|
+
column: decl.loc?.start.column || 0,
|
|
1906
|
+
message: `Do not use IIFE pattern for component initialization. Define components as plain functions.`,
|
|
1907
|
+
code: decl.toString().substring(0, 50) + '...'
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1322
1912
|
}
|
|
1323
1913
|
}
|
|
1324
|
-
}
|
|
1914
|
+
}
|
|
1325
1915
|
return violations;
|
|
1326
1916
|
}
|
|
1327
1917
|
},
|
|
1328
1918
|
{
|
|
1329
|
-
name: '
|
|
1919
|
+
name: 'no-use-reducer',
|
|
1330
1920
|
appliesTo: 'all',
|
|
1331
1921
|
test: (ast, componentName, componentSpec) => {
|
|
1332
1922
|
const violations = [];
|
|
1333
1923
|
(0, traverse_1.default)(ast, {
|
|
1334
1924
|
CallExpression(path) {
|
|
1335
|
-
|
|
1336
|
-
if (t.
|
|
1337
|
-
t.
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1925
|
+
const callee = path.node.callee;
|
|
1926
|
+
if ((t.isIdentifier(callee) && callee.name === 'useReducer') ||
|
|
1927
|
+
(t.isMemberExpression(callee) &&
|
|
1928
|
+
t.isIdentifier(callee.object) && callee.object.name === 'React' &&
|
|
1929
|
+
t.isIdentifier(callee.property) && callee.property.name === 'useReducer')) {
|
|
1930
|
+
violations.push({
|
|
1931
|
+
rule: 'no-use-reducer',
|
|
1932
|
+
severity: 'high', // High but not critical - it's a pattern violation
|
|
1933
|
+
line: path.node.loc?.start.line || 0,
|
|
1934
|
+
column: path.node.loc?.start.column || 0,
|
|
1935
|
+
message: `Component "${componentName}" uses useReducer at line ${path.node.loc?.start.line}. Components should manage state with useState and persist important settings with onSaveUserSettings.`,
|
|
1936
|
+
code: path.toString()
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
return violations;
|
|
1942
|
+
}
|
|
1943
|
+
},
|
|
1944
|
+
// New rules for the controlled component pattern
|
|
1945
|
+
{
|
|
1946
|
+
name: 'no-data-prop',
|
|
1947
|
+
appliesTo: 'all',
|
|
1948
|
+
test: (ast, componentName, componentSpec) => {
|
|
1949
|
+
const violations = [];
|
|
1950
|
+
(0, traverse_1.default)(ast, {
|
|
1951
|
+
// Check function parameters for 'data' prop
|
|
1952
|
+
FunctionDeclaration(path) {
|
|
1953
|
+
if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
|
|
1954
|
+
const param = path.node.params[0];
|
|
1955
|
+
if (t.isObjectPattern(param)) {
|
|
1956
|
+
for (const prop of param.properties) {
|
|
1957
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
|
|
1958
|
+
violations.push({
|
|
1959
|
+
rule: 'no-data-prop',
|
|
1960
|
+
severity: 'medium', // It's a pattern issue, not critical
|
|
1961
|
+
line: prop.loc?.start.line || 0,
|
|
1962
|
+
column: prop.loc?.start.column || 0,
|
|
1963
|
+
message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
|
|
1964
|
+
code: 'data prop in component signature'
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
},
|
|
1971
|
+
// Also check arrow functions
|
|
1972
|
+
VariableDeclarator(path) {
|
|
1973
|
+
if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
|
|
1974
|
+
const init = path.node.init;
|
|
1975
|
+
if (t.isArrowFunctionExpression(init) && init.params[0]) {
|
|
1976
|
+
const param = init.params[0];
|
|
1977
|
+
if (t.isObjectPattern(param)) {
|
|
1978
|
+
for (const prop of param.properties) {
|
|
1979
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'data') {
|
|
1980
|
+
violations.push({
|
|
1981
|
+
rule: 'no-data-prop',
|
|
1982
|
+
severity: 'critical',
|
|
1983
|
+
line: prop.loc?.start.line || 0,
|
|
1984
|
+
column: prop.loc?.start.column || 0,
|
|
1985
|
+
message: `Component "${componentName}" accepts generic 'data' prop. Use specific props like 'items', 'customers', etc. instead.`,
|
|
1986
|
+
code: 'data prop in component signature'
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
return violations;
|
|
1996
|
+
}
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
name: 'saved-user-settings-pattern',
|
|
2000
|
+
appliesTo: 'all',
|
|
2001
|
+
test: (ast, componentName, componentSpec) => {
|
|
2002
|
+
const violations = [];
|
|
2003
|
+
// Check for improper onSaveUserSettings usage
|
|
2004
|
+
(0, traverse_1.default)(ast, {
|
|
2005
|
+
CallExpression(path) {
|
|
2006
|
+
const callee = path.node.callee;
|
|
2007
|
+
// Check for onSaveUserSettings calls
|
|
2008
|
+
if (t.isMemberExpression(callee) &&
|
|
2009
|
+
t.isIdentifier(callee.object) && callee.object.name === 'onSaveUserSettings') {
|
|
2010
|
+
// Check if saving ephemeral state
|
|
2011
|
+
if (path.node.arguments.length > 0) {
|
|
2012
|
+
const arg = path.node.arguments[0];
|
|
2013
|
+
if (t.isObjectExpression(arg)) {
|
|
2014
|
+
for (const prop of arg.properties) {
|
|
2015
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
2016
|
+
const key = prop.key.name;
|
|
2017
|
+
const ephemeralPatterns = ['hover', 'dropdown', 'modal', 'loading', 'typing', 'draft', 'expanded', 'collapsed', 'focused'];
|
|
2018
|
+
if (ephemeralPatterns.some(pattern => key.toLowerCase().includes(pattern))) {
|
|
2019
|
+
violations.push({
|
|
2020
|
+
rule: 'saved-user-settings-pattern',
|
|
2021
|
+
severity: 'medium', // Pattern issue but not breaking
|
|
2022
|
+
line: prop.loc?.start.line || 0,
|
|
2023
|
+
column: prop.loc?.start.column || 0,
|
|
2024
|
+
message: `Saving ephemeral UI state "${key}" to savedUserSettings. Only save important user preferences.`
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
return violations;
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
name: 'library-variable-names',
|
|
2039
|
+
appliesTo: 'all',
|
|
2040
|
+
test: (ast, componentName, componentSpec) => {
|
|
2041
|
+
const violations = [];
|
|
2042
|
+
// Build a map of library names to their globalVariables
|
|
2043
|
+
const libraryGlobals = new Map();
|
|
2044
|
+
if (componentSpec?.libraries) {
|
|
2045
|
+
for (const lib of componentSpec.libraries) {
|
|
2046
|
+
// Store both the exact name and lowercase for comparison
|
|
2047
|
+
libraryGlobals.set(lib.name.toLowerCase(), lib.globalVariable);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
(0, traverse_1.default)(ast, {
|
|
2051
|
+
VariableDeclarator(path) {
|
|
2052
|
+
// Check for destructuring from a variable (library global)
|
|
2053
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
2054
|
+
const sourceVar = path.node.init.name;
|
|
2055
|
+
// Check if this looks like a library name (case-insensitive match)
|
|
2056
|
+
const matchedLib = Array.from(libraryGlobals.entries()).find(([libName, globalVar]) => sourceVar.toLowerCase() === libName ||
|
|
2057
|
+
sourceVar.toLowerCase() === globalVar.toLowerCase());
|
|
2058
|
+
if (matchedLib) {
|
|
2059
|
+
const [libName, correctGlobal] = matchedLib;
|
|
2060
|
+
if (sourceVar !== correctGlobal) {
|
|
2061
|
+
violations.push({
|
|
2062
|
+
rule: 'library-variable-names',
|
|
2063
|
+
severity: 'critical',
|
|
2064
|
+
line: path.node.loc?.start.line || 0,
|
|
2065
|
+
column: path.node.loc?.start.column || 0,
|
|
2066
|
+
message: `Incorrect library global variable "${sourceVar}". Use the exact globalVariable from the library spec: "${correctGlobal}". Change "const { ... } = ${sourceVar};" to "const { ... } = ${correctGlobal};"`
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
// Check for self-assignment (const chroma = chroma)
|
|
2072
|
+
if (t.isIdentifier(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
2073
|
+
const idName = path.node.id.name;
|
|
2074
|
+
const initName = path.node.init.name;
|
|
2075
|
+
if (idName === initName) {
|
|
2076
|
+
// Check if this is a library global
|
|
2077
|
+
const isLibraryGlobal = Array.from(libraryGlobals.values()).some(global => global === idName);
|
|
2078
|
+
if (isLibraryGlobal) {
|
|
2079
|
+
violations.push({
|
|
2080
|
+
rule: 'library-variable-names',
|
|
2081
|
+
severity: 'critical',
|
|
2082
|
+
line: path.node.loc?.start.line || 0,
|
|
2083
|
+
column: path.node.loc?.start.column || 0,
|
|
2084
|
+
message: `Self-assignment of library global "${idName}". This variable is already available as a global from the library. Remove this line entirely - the library global is already accessible.`
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
return violations;
|
|
2092
|
+
}
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: 'pass-standard-props',
|
|
2096
|
+
appliesTo: 'all',
|
|
2097
|
+
test: (ast, componentName, componentSpec) => {
|
|
2098
|
+
const violations = [];
|
|
2099
|
+
const requiredProps = ['styles', 'utilities', 'components'];
|
|
2100
|
+
// ONLY check components that are explicitly in our dependencies
|
|
2101
|
+
// Do NOT check library components, HTML elements, or anything else
|
|
2102
|
+
const ourComponentNames = new Set();
|
|
2103
|
+
// Only add components from the componentSpec.dependencies array
|
|
2104
|
+
if (componentSpec?.dependencies && Array.isArray(componentSpec.dependencies)) {
|
|
2105
|
+
for (const dep of componentSpec.dependencies) {
|
|
2106
|
+
if (dep && dep.name) {
|
|
2107
|
+
ourComponentNames.add(dep.name);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
// If there are no dependencies, skip this rule entirely
|
|
2112
|
+
if (ourComponentNames.size === 0) {
|
|
2113
|
+
return violations;
|
|
2114
|
+
}
|
|
2115
|
+
// Now check only our dependency components for standard props
|
|
2116
|
+
(0, traverse_1.default)(ast, {
|
|
2117
|
+
JSXElement(path) {
|
|
2118
|
+
const openingElement = path.node.openingElement;
|
|
2119
|
+
// Only check if it's one of our dependency components
|
|
2120
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
2121
|
+
const elementName = openingElement.name.name;
|
|
2122
|
+
// CRITICAL: Only check if this component is in our dependencies
|
|
2123
|
+
// Skip all library components (like TableHead, PieChart, etc.)
|
|
2124
|
+
// Skip all HTML elements
|
|
2125
|
+
if (!ourComponentNames.has(elementName)) {
|
|
2126
|
+
return; // Skip this element - it's not one of our dependencies
|
|
2127
|
+
}
|
|
2128
|
+
const passedProps = new Set();
|
|
2129
|
+
// Collect all props being passed
|
|
2130
|
+
for (const attr of openingElement.attributes) {
|
|
2131
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
2132
|
+
passedProps.add(attr.name.name);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
// Check if required props are missing
|
|
2136
|
+
const missingProps = requiredProps.filter(prop => !passedProps.has(prop));
|
|
2137
|
+
if (missingProps.length > 0) {
|
|
2138
|
+
violations.push({
|
|
2139
|
+
rule: 'pass-standard-props',
|
|
2140
|
+
severity: 'critical',
|
|
2141
|
+
line: openingElement.loc?.start.line || 0,
|
|
2142
|
+
column: openingElement.loc?.start.column || 0,
|
|
2143
|
+
message: `Dependency component "${elementName}" is missing required props: ${missingProps.join(', ')}. Components from dependencies must receive styles, utilities, and components props.`,
|
|
2144
|
+
code: `<${elementName} ... />`
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
return violations;
|
|
2151
|
+
}
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
name: 'no-child-implementation',
|
|
2155
|
+
appliesTo: 'root',
|
|
2156
|
+
test: (ast, componentName, componentSpec) => {
|
|
2157
|
+
const violations = [];
|
|
2158
|
+
const rootFunctionName = componentName;
|
|
2159
|
+
const declaredFunctions = [];
|
|
2160
|
+
// First pass: collect all function declarations
|
|
2161
|
+
(0, traverse_1.default)(ast, {
|
|
2162
|
+
FunctionDeclaration(path) {
|
|
2163
|
+
if (path.node.id) {
|
|
2164
|
+
declaredFunctions.push(path.node.id.name);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
// If there are multiple function declarations and they look like components
|
|
2169
|
+
// (start with capital letter), it's likely implementing children
|
|
2170
|
+
const componentFunctions = declaredFunctions.filter(name => name !== rootFunctionName && /^[A-Z]/.test(name));
|
|
2171
|
+
if (componentFunctions.length > 0) {
|
|
2172
|
+
violations.push({
|
|
2173
|
+
rule: 'no-child-implementation',
|
|
2174
|
+
severity: 'critical',
|
|
2175
|
+
line: 1,
|
|
2176
|
+
column: 0,
|
|
2177
|
+
message: `Root component file contains child component implementations: ${componentFunctions.join(', ')}. Root should only reference child components, not implement them.`,
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
return violations;
|
|
2181
|
+
}
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
name: 'undefined-component-usage',
|
|
2185
|
+
appliesTo: 'all',
|
|
2186
|
+
test: (ast, componentName, componentSpec) => {
|
|
2187
|
+
const violations = [];
|
|
2188
|
+
const componentsFromProps = new Set();
|
|
2189
|
+
const componentsUsedInJSX = new Set();
|
|
2190
|
+
let hasComponentsProp = false;
|
|
2191
|
+
(0, traverse_1.default)(ast, {
|
|
2192
|
+
// First, find what's destructured from the components prop
|
|
2193
|
+
VariableDeclarator(path) {
|
|
2194
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
2195
|
+
// Check if destructuring from 'components'
|
|
2196
|
+
if (path.node.init.name === 'components') {
|
|
2197
|
+
hasComponentsProp = true;
|
|
2198
|
+
for (const prop of path.node.id.properties) {
|
|
2199
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
2200
|
+
componentsFromProps.add(prop.key.name);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
},
|
|
2206
|
+
// Also check object destructuring in function parameters
|
|
2207
|
+
FunctionDeclaration(path) {
|
|
2208
|
+
if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
|
|
2209
|
+
const param = path.node.params[0];
|
|
2210
|
+
if (t.isObjectPattern(param)) {
|
|
2211
|
+
for (const prop of param.properties) {
|
|
2212
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === 'components') {
|
|
2213
|
+
hasComponentsProp = true;
|
|
2214
|
+
// Look for nested destructuring like { components: { A, B } }
|
|
2215
|
+
if (t.isObjectPattern(prop.value)) {
|
|
2216
|
+
for (const innerProp of prop.value.properties) {
|
|
2217
|
+
if (t.isObjectProperty(innerProp) && t.isIdentifier(innerProp.key)) {
|
|
2218
|
+
componentsFromProps.add(innerProp.key.name);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
2227
|
+
// Track JSX element usage
|
|
2228
|
+
JSXElement(path) {
|
|
2229
|
+
const openingElement = path.node.openingElement;
|
|
2230
|
+
if (t.isJSXIdentifier(openingElement.name) && /^[A-Z]/.test(openingElement.name.name)) {
|
|
2231
|
+
const componentName = openingElement.name.name;
|
|
2232
|
+
// Only track if it's from our destructured components
|
|
2233
|
+
if (componentsFromProps.has(componentName)) {
|
|
2234
|
+
componentsUsedInJSX.add(componentName);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
// Only check if we found a components prop
|
|
2240
|
+
if (hasComponentsProp && componentsFromProps.size > 0) {
|
|
2241
|
+
// Find components that are destructured but never used
|
|
2242
|
+
const unusedComponents = Array.from(componentsFromProps).filter(comp => !componentsUsedInJSX.has(comp));
|
|
2243
|
+
if (unusedComponents.length > 0) {
|
|
2244
|
+
violations.push({
|
|
2245
|
+
rule: 'undefined-component-usage',
|
|
2246
|
+
severity: 'low',
|
|
2247
|
+
line: 1,
|
|
2248
|
+
column: 0,
|
|
2249
|
+
message: `Component destructures ${unusedComponents.join(', ')} from components prop but never uses them. These may be missing from the component spec's dependencies array.`
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return violations;
|
|
2254
|
+
}
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
name: 'unsafe-array-access',
|
|
2258
|
+
appliesTo: 'all',
|
|
2259
|
+
test: (ast, componentName, componentSpec) => {
|
|
2260
|
+
const violations = [];
|
|
2261
|
+
(0, traverse_1.default)(ast, {
|
|
2262
|
+
MemberExpression(path) {
|
|
2263
|
+
// Check for array[index] patterns
|
|
2264
|
+
if (t.isNumericLiteral(path.node.property) ||
|
|
2265
|
+
(t.isIdentifier(path.node.property) && path.node.computed && /^\d+$/.test(path.node.property.name))) {
|
|
2266
|
+
// Look for patterns like: someArray[0].method()
|
|
2267
|
+
const parent = path.parent;
|
|
2268
|
+
if (t.isMemberExpression(parent) && parent.object === path.node) {
|
|
2269
|
+
const code = path.toString();
|
|
2270
|
+
// Check if it's an array access followed by a method call
|
|
2271
|
+
if (/\[\d+\]\.\w+/.test(code)) {
|
|
2272
|
+
violations.push({
|
|
2273
|
+
rule: 'unsafe-array-access',
|
|
2274
|
+
severity: 'critical',
|
|
2275
|
+
line: path.node.loc?.start.line || 0,
|
|
2276
|
+
column: path.node.loc?.start.column || 0,
|
|
2277
|
+
message: `Unsafe array access: ${code}. Check array bounds before accessing elements.`,
|
|
2278
|
+
code: code
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
return violations;
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2288
|
+
{
|
|
2289
|
+
name: 'array-reduce-safety',
|
|
2290
|
+
appliesTo: 'all',
|
|
2291
|
+
test: (ast, componentName, componentSpec) => {
|
|
2292
|
+
const violations = [];
|
|
2293
|
+
(0, traverse_1.default)(ast, {
|
|
2294
|
+
CallExpression(path) {
|
|
2295
|
+
// Check for .reduce() calls
|
|
2296
|
+
if (t.isMemberExpression(path.node.callee) &&
|
|
2297
|
+
t.isIdentifier(path.node.callee.property) &&
|
|
2298
|
+
path.node.callee.property.name === 'reduce') {
|
|
2299
|
+
// Check if the array might be empty
|
|
2300
|
+
const arrayExpression = path.node.callee.object;
|
|
2301
|
+
const code = path.toString();
|
|
2302
|
+
// Look for patterns that suggest no safety check
|
|
2303
|
+
const hasInitialValue = path.node.arguments.length > 1;
|
|
2304
|
+
if (!hasInitialValue) {
|
|
2305
|
+
violations.push({
|
|
2306
|
+
rule: 'array-reduce-safety',
|
|
2307
|
+
severity: 'low',
|
|
2308
|
+
line: path.node.loc?.start.line || 0,
|
|
2309
|
+
column: path.node.loc?.start.column || 0,
|
|
2310
|
+
message: `reduce() without initial value may fail on empty arrays: ${code}`,
|
|
2311
|
+
code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
// Check for reduce on array access like arr[0].reduce()
|
|
2315
|
+
if (t.isMemberExpression(arrayExpression) &&
|
|
2316
|
+
(t.isNumericLiteral(arrayExpression.property) ||
|
|
2317
|
+
(t.isIdentifier(arrayExpression.property) && arrayExpression.computed))) {
|
|
2318
|
+
violations.push({
|
|
2319
|
+
rule: 'array-reduce-safety',
|
|
2320
|
+
severity: 'critical',
|
|
2321
|
+
line: path.node.loc?.start.line || 0,
|
|
2322
|
+
column: path.node.loc?.start.column || 0,
|
|
2323
|
+
message: `reduce() on array element access is unsafe: ${code}`,
|
|
2324
|
+
code: code.substring(0, 100) + (code.length > 100 ? '...' : '')
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
return violations;
|
|
2331
|
+
}
|
|
2332
|
+
},
|
|
2333
|
+
// {
|
|
2334
|
+
// name: 'parent-event-callback-usage',
|
|
2335
|
+
// appliesTo: 'child',
|
|
1376
2336
|
// test: (ast: t.File, componentName: string) => {
|
|
1377
2337
|
// const violations: Violation[] = [];
|
|
1378
2338
|
// const eventCallbacks = new Map<string, { line: number; column: number }>();
|
|
@@ -1505,152 +2465,716 @@ ComponentLinter.universalComponentRules = [
|
|
|
1505
2465
|
// }
|
|
1506
2466
|
// },
|
|
1507
2467
|
{
|
|
1508
|
-
name: 'property-name-consistency',
|
|
2468
|
+
name: 'property-name-consistency',
|
|
2469
|
+
appliesTo: 'all',
|
|
2470
|
+
test: (ast, componentName, componentSpec) => {
|
|
2471
|
+
const violations = [];
|
|
2472
|
+
const dataTransformations = new Map();
|
|
2473
|
+
const propertyAccesses = new Map(); // variable -> accessed properties
|
|
2474
|
+
// Track data transformations (especially in map functions)
|
|
2475
|
+
(0, traverse_1.default)(ast, {
|
|
2476
|
+
CallExpression(path) {
|
|
2477
|
+
// Look for array.map transformations
|
|
2478
|
+
if (t.isMemberExpression(path.node.callee) &&
|
|
2479
|
+
t.isIdentifier(path.node.callee.property) &&
|
|
2480
|
+
path.node.callee.property.name === 'map') {
|
|
2481
|
+
const mapArg = path.node.arguments[0];
|
|
2482
|
+
if (mapArg && (t.isArrowFunctionExpression(mapArg) || t.isFunctionExpression(mapArg))) {
|
|
2483
|
+
const param = mapArg.params[0];
|
|
2484
|
+
if (t.isIdentifier(param)) {
|
|
2485
|
+
const paramName = param.name;
|
|
2486
|
+
const originalProps = new Set();
|
|
2487
|
+
const transformedProps = new Set();
|
|
2488
|
+
// Check the return value
|
|
2489
|
+
let returnValue = null;
|
|
2490
|
+
if (t.isArrowFunctionExpression(mapArg)) {
|
|
2491
|
+
if (t.isObjectExpression(mapArg.body)) {
|
|
2492
|
+
returnValue = mapArg.body;
|
|
2493
|
+
}
|
|
2494
|
+
else if (t.isBlockStatement(mapArg.body)) {
|
|
2495
|
+
// Find return statement
|
|
2496
|
+
for (const stmt of mapArg.body.body) {
|
|
2497
|
+
if (t.isReturnStatement(stmt) && stmt.argument) {
|
|
2498
|
+
returnValue = stmt.argument;
|
|
2499
|
+
break;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
// Analyze object mapping
|
|
2505
|
+
if (returnValue && t.isObjectExpression(returnValue)) {
|
|
2506
|
+
for (const prop of returnValue.properties) {
|
|
2507
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
2508
|
+
transformedProps.add(prop.key.name);
|
|
2509
|
+
// Check if value is a member expression from the parameter
|
|
2510
|
+
if (t.isMemberExpression(prop.value) &&
|
|
2511
|
+
t.isIdentifier(prop.value.object) &&
|
|
2512
|
+
prop.value.object.name === paramName &&
|
|
2513
|
+
t.isIdentifier(prop.value.property)) {
|
|
2514
|
+
originalProps.add(prop.value.property.name);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
// Store the transformation if we found property mappings
|
|
2520
|
+
if (transformedProps.size > 0) {
|
|
2521
|
+
// Find the variable being assigned
|
|
2522
|
+
let parentPath = path.parentPath;
|
|
2523
|
+
while (parentPath && !t.isVariableDeclarator(parentPath.node) && !t.isCallExpression(parentPath.node)) {
|
|
2524
|
+
parentPath = parentPath.parentPath;
|
|
2525
|
+
}
|
|
2526
|
+
if (parentPath && t.isCallExpression(parentPath.node)) {
|
|
2527
|
+
// Check for setState calls
|
|
2528
|
+
if (t.isIdentifier(parentPath.node.callee) && /^set[A-Z]/.test(parentPath.node.callee.name)) {
|
|
2529
|
+
const stateName = parentPath.node.callee.name.replace(/^set/, '');
|
|
2530
|
+
const varName = stateName.charAt(0).toLowerCase() + stateName.slice(1);
|
|
2531
|
+
dataTransformations.set(varName, {
|
|
2532
|
+
originalProps,
|
|
2533
|
+
transformedProps,
|
|
2534
|
+
location: {
|
|
2535
|
+
line: path.node.loc?.start.line || 0,
|
|
2536
|
+
column: path.node.loc?.start.column || 0
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
},
|
|
2546
|
+
// Track property accesses
|
|
2547
|
+
MemberExpression(path) {
|
|
2548
|
+
if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
|
|
2549
|
+
const objName = path.node.object.name;
|
|
2550
|
+
const propName = path.node.property.name;
|
|
2551
|
+
if (!propertyAccesses.has(objName)) {
|
|
2552
|
+
propertyAccesses.set(objName, new Set());
|
|
2553
|
+
}
|
|
2554
|
+
propertyAccesses.get(objName).add(propName);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
// Check for mismatches
|
|
2559
|
+
for (const [varName, transformation] of dataTransformations) {
|
|
2560
|
+
const accesses = propertyAccesses.get(varName);
|
|
2561
|
+
if (accesses) {
|
|
2562
|
+
for (const accessedProp of accesses) {
|
|
2563
|
+
// Check if accessed property exists in transformed props
|
|
2564
|
+
if (!transformation.transformedProps.has(accessedProp)) {
|
|
2565
|
+
// Check if it's trying to use original prop name
|
|
2566
|
+
const matchingOriginal = Array.from(transformation.originalProps).find(orig => orig.toLowerCase() === accessedProp.toLowerCase());
|
|
2567
|
+
if (matchingOriginal) {
|
|
2568
|
+
// Find the transformed name
|
|
2569
|
+
const transformedName = Array.from(transformation.transformedProps).find(t => t.toLowerCase() === accessedProp.toLowerCase());
|
|
2570
|
+
violations.push({
|
|
2571
|
+
rule: 'property-name-consistency',
|
|
2572
|
+
severity: 'critical',
|
|
2573
|
+
line: transformation.location.line,
|
|
2574
|
+
column: transformation.location.column,
|
|
2575
|
+
message: `Property name mismatch: data transformed with different casing. Accessing '${accessedProp}' but property was transformed to '${transformedName || 'different name'}'`,
|
|
2576
|
+
code: `Transform uses '${Array.from(transformation.transformedProps).join(', ')}' but code accesses '${accessedProp}'`
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return violations;
|
|
2584
|
+
}
|
|
2585
|
+
},
|
|
2586
|
+
// New rules to align with AI linter
|
|
2587
|
+
{
|
|
2588
|
+
name: 'noisy-settings-updates',
|
|
1509
2589
|
appliesTo: 'all',
|
|
1510
2590
|
test: (ast, componentName, componentSpec) => {
|
|
1511
2591
|
const violations = [];
|
|
1512
|
-
const dataTransformations = new Map();
|
|
1513
|
-
const propertyAccesses = new Map(); // variable -> accessed properties
|
|
1514
|
-
// Track data transformations (especially in map functions)
|
|
1515
2592
|
(0, traverse_1.default)(ast, {
|
|
1516
2593
|
CallExpression(path) {
|
|
1517
|
-
//
|
|
1518
|
-
if (t.
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2594
|
+
// Check for onSaveUserSettings calls
|
|
2595
|
+
if (t.isOptionalCallExpression(path.node) || t.isCallExpression(path.node)) {
|
|
2596
|
+
const callee = path.node.callee;
|
|
2597
|
+
if (t.isIdentifier(callee) && callee.name === 'onSaveUserSettings') {
|
|
2598
|
+
// Check if this is inside an onChange/onInput handler
|
|
2599
|
+
let parent = path.getFunctionParent();
|
|
2600
|
+
if (parent) {
|
|
2601
|
+
const funcName = ComponentLinter.getFunctionName(parent);
|
|
2602
|
+
if (funcName && (funcName.includes('Change') || funcName.includes('Input'))) {
|
|
2603
|
+
// Check if it's not debounced or on blur
|
|
2604
|
+
const parentBody = parent.node.body;
|
|
2605
|
+
const hasDebounce = parentBody && parentBody.toString().includes('debounce');
|
|
2606
|
+
const hasTimeout = parentBody && parentBody.toString().includes('setTimeout');
|
|
2607
|
+
if (!hasDebounce && !hasTimeout) {
|
|
2608
|
+
violations.push({
|
|
2609
|
+
rule: 'noisy-settings-updates',
|
|
2610
|
+
severity: 'critical',
|
|
2611
|
+
line: path.node.loc?.start.line || 0,
|
|
2612
|
+
column: path.node.loc?.start.column || 0,
|
|
2613
|
+
message: `Saving settings on every change/keystroke. Save on blur, submit, or after debouncing.`
|
|
2614
|
+
});
|
|
1533
2615
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
return violations;
|
|
2623
|
+
}
|
|
2624
|
+
},
|
|
2625
|
+
{
|
|
2626
|
+
name: 'prop-state-sync',
|
|
2627
|
+
appliesTo: 'all',
|
|
2628
|
+
test: (ast, componentName, componentSpec) => {
|
|
2629
|
+
const violations = [];
|
|
2630
|
+
(0, traverse_1.default)(ast, {
|
|
2631
|
+
CallExpression(path) {
|
|
2632
|
+
if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useEffect') {
|
|
2633
|
+
const effectBody = path.node.arguments[0];
|
|
2634
|
+
const deps = path.node.arguments[1];
|
|
2635
|
+
if (effectBody && (t.isArrowFunctionExpression(effectBody) || t.isFunctionExpression(effectBody))) {
|
|
2636
|
+
const bodyString = effectBody.body.toString();
|
|
2637
|
+
// Check if it's setting state based on props
|
|
2638
|
+
const hasSetState = /set[A-Z]\w*\s*\(/.test(bodyString);
|
|
2639
|
+
const depsString = deps ? deps.toString() : '';
|
|
2640
|
+
// Check if deps include prop-like names
|
|
2641
|
+
const propPatterns = ['Prop', 'value', 'data', 'items'];
|
|
2642
|
+
const hasPropDeps = propPatterns.some(p => depsString.includes(p));
|
|
2643
|
+
if (hasSetState && hasPropDeps && !bodyString.includes('async')) {
|
|
2644
|
+
violations.push({
|
|
2645
|
+
rule: 'prop-state-sync',
|
|
2646
|
+
severity: 'critical',
|
|
2647
|
+
line: path.node.loc?.start.line || 0,
|
|
2648
|
+
column: path.node.loc?.start.column || 0,
|
|
2649
|
+
message: 'Syncing props to internal state with useEffect creates dual state management',
|
|
2650
|
+
code: path.toString().substring(0, 100)
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
return violations;
|
|
2658
|
+
}
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
name: 'performance-memoization',
|
|
2662
|
+
appliesTo: 'all',
|
|
2663
|
+
test: (ast, componentName, componentSpec) => {
|
|
2664
|
+
const violations = [];
|
|
2665
|
+
const memoizedValues = new Set();
|
|
2666
|
+
// Collect memoized values
|
|
2667
|
+
(0, traverse_1.default)(ast, {
|
|
2668
|
+
CallExpression(path) {
|
|
2669
|
+
if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useMemo') {
|
|
2670
|
+
// Find the variable being assigned
|
|
2671
|
+
if (t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)) {
|
|
2672
|
+
memoizedValues.add(path.parent.id.name);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
// Check for expensive operations without memoization
|
|
2678
|
+
(0, traverse_1.default)(ast, {
|
|
2679
|
+
CallExpression(path) {
|
|
2680
|
+
if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property)) {
|
|
2681
|
+
const method = path.node.callee.property.name;
|
|
2682
|
+
// Check for expensive array operations
|
|
2683
|
+
if (['filter', 'sort', 'map', 'reduce'].includes(method)) {
|
|
2684
|
+
// Check if this is inside a variable declaration
|
|
2685
|
+
let parentPath = path.parentPath;
|
|
2686
|
+
while (parentPath && !t.isVariableDeclarator(parentPath.node)) {
|
|
2687
|
+
parentPath = parentPath.parentPath;
|
|
2688
|
+
}
|
|
2689
|
+
if (parentPath && t.isVariableDeclarator(parentPath.node) && t.isIdentifier(parentPath.node.id)) {
|
|
2690
|
+
const varName = parentPath.node.id.name;
|
|
2691
|
+
// Check if it's not memoized
|
|
2692
|
+
if (!memoizedValues.has(varName)) {
|
|
2693
|
+
// Check if it's in the render method (not in event handlers)
|
|
2694
|
+
let funcParent = path.getFunctionParent();
|
|
2695
|
+
if (funcParent) {
|
|
2696
|
+
const funcName = ComponentLinter.getFunctionName(funcParent);
|
|
2697
|
+
if (!funcName || funcName === componentName) {
|
|
2698
|
+
violations.push({
|
|
2699
|
+
rule: 'performance-memoization',
|
|
2700
|
+
severity: 'low', // Just a suggestion, not mandatory
|
|
2701
|
+
line: path.node.loc?.start.line || 0,
|
|
2702
|
+
column: path.node.loc?.start.column || 0,
|
|
2703
|
+
message: `Expensive ${method} operation without memoization. Consider using useMemo.`,
|
|
2704
|
+
code: `const ${varName} = ...${method}(...)`
|
|
2705
|
+
});
|
|
1541
2706
|
}
|
|
1542
2707
|
}
|
|
1543
2708
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
2709
|
+
}
|
|
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
|
|
2722
|
+
violations.push({
|
|
2723
|
+
rule: 'performance-memoization',
|
|
2724
|
+
severity: 'low', // Just a suggestion
|
|
2725
|
+
line: path.node.loc?.start.line || 0,
|
|
2726
|
+
column: path.node.loc?.start.column || 0,
|
|
2727
|
+
message: 'Static array/object recreated on every render. Consider using useMemo.',
|
|
2728
|
+
code: `const ${varName} = ${path.node.init.type === 'ArrayExpression' ? '[...]' : '{...}'}`
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
return violations;
|
|
2736
|
+
}
|
|
2737
|
+
},
|
|
2738
|
+
{
|
|
2739
|
+
name: 'child-state-management',
|
|
2740
|
+
appliesTo: 'all',
|
|
2741
|
+
test: (ast, componentName, componentSpec) => {
|
|
2742
|
+
const violations = [];
|
|
2743
|
+
(0, traverse_1.default)(ast, {
|
|
2744
|
+
CallExpression(path) {
|
|
2745
|
+
if (t.isIdentifier(path.node.callee) && path.node.callee.name === 'useState') {
|
|
2746
|
+
// Check if the state name suggests child component state
|
|
2747
|
+
if (t.isVariableDeclarator(path.parent) && t.isArrayPattern(path.parent.id)) {
|
|
2748
|
+
const stateNameNode = path.parent.id.elements[0];
|
|
2749
|
+
if (t.isIdentifier(stateNameNode)) {
|
|
2750
|
+
const stateName = stateNameNode.name;
|
|
2751
|
+
// Check for patterns suggesting child state management
|
|
2752
|
+
const childPatterns = [
|
|
2753
|
+
/^child/i,
|
|
2754
|
+
/Table\w*State/,
|
|
2755
|
+
/Panel\w*State/,
|
|
2756
|
+
/Modal\w*State/,
|
|
2757
|
+
/\w+Component\w*/
|
|
2758
|
+
];
|
|
2759
|
+
if (childPatterns.some(pattern => pattern.test(stateName))) {
|
|
2760
|
+
violations.push({
|
|
2761
|
+
rule: 'child-state-management',
|
|
2762
|
+
severity: 'critical',
|
|
2763
|
+
line: path.node.loc?.start.line || 0,
|
|
2764
|
+
column: path.node.loc?.start.column || 0,
|
|
2765
|
+
message: `Component trying to manage child component state: ${stateName}. Child components manage their own state!`,
|
|
2766
|
+
code: `const [${stateName}, ...] = useState(...)`
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
return violations;
|
|
2775
|
+
}
|
|
2776
|
+
},
|
|
2777
|
+
{
|
|
2778
|
+
name: 'server-reload-on-client-operation',
|
|
2779
|
+
appliesTo: 'all',
|
|
2780
|
+
test: (ast, componentName, componentSpec) => {
|
|
2781
|
+
const violations = [];
|
|
2782
|
+
(0, traverse_1.default)(ast, {
|
|
2783
|
+
CallExpression(path) {
|
|
2784
|
+
const callee = path.node.callee;
|
|
2785
|
+
// Look for data loading functions
|
|
2786
|
+
if (t.isIdentifier(callee) &&
|
|
2787
|
+
(callee.name.includes('load') || callee.name.includes('fetch'))) {
|
|
2788
|
+
// Check if it's called in sort/filter handlers
|
|
2789
|
+
let funcParent = path.getFunctionParent();
|
|
2790
|
+
if (funcParent) {
|
|
2791
|
+
const funcName = ComponentLinter.getFunctionName(funcParent);
|
|
2792
|
+
if (funcName &&
|
|
2793
|
+
(funcName.includes('Sort') || funcName.includes('Filter') ||
|
|
2794
|
+
funcName.includes('handleSort') || funcName.includes('handleFilter'))) {
|
|
2795
|
+
violations.push({
|
|
2796
|
+
rule: 'server-reload-on-client-operation',
|
|
2797
|
+
severity: 'critical',
|
|
2798
|
+
line: path.node.loc?.start.line || 0,
|
|
2799
|
+
column: path.node.loc?.start.column || 0,
|
|
2800
|
+
message: 'Reloading data from server on sort/filter. Use useMemo for client-side operations.',
|
|
2801
|
+
code: `${funcName} calls ${callee.name}`
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
return violations;
|
|
2809
|
+
}
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
name: 'runview-runquery-valid-properties',
|
|
2813
|
+
appliesTo: 'all',
|
|
2814
|
+
test: (ast, componentName, componentSpec) => {
|
|
2815
|
+
const violations = [];
|
|
2816
|
+
// Valid properties for RunView/RunViews
|
|
2817
|
+
const validRunViewProps = new Set([
|
|
2818
|
+
'ViewID', 'ViewName', 'EntityName', 'ExtraFilter', 'OrderBy', 'Fields',
|
|
2819
|
+
'MaxRows', 'StartRow', 'ResultType', 'UserSearchString', 'ForceAuditLog', 'AuditLogDescription',
|
|
2820
|
+
'ResultType'
|
|
2821
|
+
]);
|
|
2822
|
+
// Valid properties for RunQuery
|
|
2823
|
+
const validRunQueryProps = new Set([
|
|
2824
|
+
'QueryID', 'QueryName', 'CategoryID', 'CategoryPath', 'Parameters', 'MaxRows', 'StartRow', 'ForceAuditLog', 'AuditLogDescription'
|
|
2825
|
+
]);
|
|
2826
|
+
(0, traverse_1.default)(ast, {
|
|
2827
|
+
CallExpression(path) {
|
|
2828
|
+
const callee = path.node.callee;
|
|
2829
|
+
// Check for utilities.rv.RunView or utilities.rv.RunViews
|
|
2830
|
+
if (t.isMemberExpression(callee) &&
|
|
2831
|
+
t.isMemberExpression(callee.object) &&
|
|
2832
|
+
t.isIdentifier(callee.object.object) &&
|
|
2833
|
+
callee.object.object.name === 'utilities' &&
|
|
2834
|
+
t.isIdentifier(callee.object.property) &&
|
|
2835
|
+
callee.object.property.name === 'rv' &&
|
|
2836
|
+
t.isIdentifier(callee.property)) {
|
|
2837
|
+
const methodName = callee.property.name;
|
|
2838
|
+
if (methodName === 'RunView' || methodName === 'RunViews') {
|
|
2839
|
+
// Check that first parameter exists
|
|
2840
|
+
if (!path.node.arguments[0]) {
|
|
2841
|
+
violations.push({
|
|
2842
|
+
rule: 'runview-runquery-valid-properties',
|
|
2843
|
+
severity: 'critical',
|
|
2844
|
+
line: path.node.loc?.start.line || 0,
|
|
2845
|
+
column: path.node.loc?.start.column || 0,
|
|
2846
|
+
message: `${methodName} requires a ${methodName === 'RunViews' ? 'array of RunViewParams objects' : 'RunViewParams object'} as the first parameter.`,
|
|
2847
|
+
code: `${methodName}()`
|
|
2848
|
+
});
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
// Get the config object(s)
|
|
2852
|
+
let configs = [];
|
|
2853
|
+
let hasValidFirstParam = false;
|
|
2854
|
+
if (methodName === 'RunViews') {
|
|
2855
|
+
// RunViews takes an array of configs
|
|
2856
|
+
if (t.isArrayExpression(path.node.arguments[0])) {
|
|
2857
|
+
hasValidFirstParam = true;
|
|
2858
|
+
configs = path.node.arguments[0].elements
|
|
2859
|
+
.filter((e) => t.isObjectExpression(e));
|
|
2860
|
+
}
|
|
2861
|
+
else {
|
|
2862
|
+
violations.push({
|
|
2863
|
+
rule: 'runview-runquery-valid-properties',
|
|
2864
|
+
severity: 'critical',
|
|
2865
|
+
line: path.node.arguments[0].loc?.start.line || 0,
|
|
2866
|
+
column: path.node.arguments[0].loc?.start.column || 0,
|
|
2867
|
+
message: `RunViews expects an array of RunViewParams objects, not a ${t.isObjectExpression(path.node.arguments[0]) ? 'single object' : 'non-array'}. Use: RunViews([{ EntityName: 'Entity1' }, { EntityName: 'Entity2' }])`,
|
|
2868
|
+
code: path.toString().substring(0, 100)
|
|
2869
|
+
});
|
|
1558
2870
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
2871
|
+
}
|
|
2872
|
+
else if (methodName === 'RunView') {
|
|
2873
|
+
// RunView takes a single config
|
|
2874
|
+
if (t.isObjectExpression(path.node.arguments[0])) {
|
|
2875
|
+
hasValidFirstParam = true;
|
|
2876
|
+
configs = [path.node.arguments[0]];
|
|
2877
|
+
}
|
|
2878
|
+
else {
|
|
2879
|
+
const argType = t.isStringLiteral(path.node.arguments[0]) ? 'string' :
|
|
2880
|
+
t.isArrayExpression(path.node.arguments[0]) ? 'array' :
|
|
2881
|
+
t.isIdentifier(path.node.arguments[0]) ? 'identifier' :
|
|
2882
|
+
'non-object';
|
|
2883
|
+
violations.push({
|
|
2884
|
+
rule: 'runview-runquery-valid-properties',
|
|
2885
|
+
severity: 'critical',
|
|
2886
|
+
line: path.node.arguments[0].loc?.start.line || 0,
|
|
2887
|
+
column: path.node.arguments[0].loc?.start.column || 0,
|
|
2888
|
+
message: `RunView expects a RunViewParams object, not ${argType === 'array' ? 'an' : 'a'} ${argType}. Use: RunView({ EntityName: 'YourEntity' }) or for multiple use RunViews([...])`,
|
|
2889
|
+
code: path.toString().substring(0, 100)
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
if (!hasValidFirstParam) {
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
// Check each config for invalid properties and required fields
|
|
2897
|
+
for (const config of configs) {
|
|
2898
|
+
// Check for required properties (must have ViewID, ViewName, ViewEntity, or EntityName)
|
|
2899
|
+
let hasViewID = false;
|
|
2900
|
+
let hasViewName = false;
|
|
2901
|
+
let hasViewEntity = false;
|
|
2902
|
+
let hasEntityName = false;
|
|
2903
|
+
for (const prop of config.properties) {
|
|
2904
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
2905
|
+
const propName = prop.key.name;
|
|
2906
|
+
if (propName === 'ViewID')
|
|
2907
|
+
hasViewID = true;
|
|
2908
|
+
if (propName === 'ViewName')
|
|
2909
|
+
hasViewName = true;
|
|
2910
|
+
if (propName === 'ViewEntity')
|
|
2911
|
+
hasViewEntity = true;
|
|
2912
|
+
if (propName === 'EntityName')
|
|
2913
|
+
hasEntityName = true;
|
|
2914
|
+
if (!validRunViewProps.has(propName)) {
|
|
2915
|
+
// Special error messages for common mistakes
|
|
2916
|
+
let message = `Invalid property '${propName}' on ${methodName}. Valid properties: ${Array.from(validRunViewProps).join(', ')}`;
|
|
2917
|
+
let fix = `Remove '${propName}' property`;
|
|
2918
|
+
if (propName === 'Parameters') {
|
|
2919
|
+
message = `${methodName} does not support 'Parameters'. Use 'ExtraFilter' for WHERE clauses.`;
|
|
2920
|
+
fix = `Replace 'Parameters' with 'ExtraFilter' and format as SQL WHERE clause`;
|
|
2921
|
+
}
|
|
2922
|
+
else if (propName === 'GroupBy') {
|
|
2923
|
+
message = `${methodName} does not support 'GroupBy'. Use RunQuery with a pre-defined query for aggregations.`;
|
|
2924
|
+
fix = `Remove 'GroupBy' and use RunQuery instead for aggregated data`;
|
|
2925
|
+
}
|
|
2926
|
+
else if (propName === 'Having') {
|
|
2927
|
+
message = `${methodName} does not support 'Having'. Use RunQuery with a pre-defined query.`;
|
|
2928
|
+
fix = `Remove 'Having' and use RunQuery instead`;
|
|
2929
|
+
}
|
|
2930
|
+
violations.push({
|
|
2931
|
+
rule: 'runview-runquery-valid-properties',
|
|
2932
|
+
severity: 'critical',
|
|
2933
|
+
line: prop.loc?.start.line || 0,
|
|
2934
|
+
column: prop.loc?.start.column || 0,
|
|
2935
|
+
message,
|
|
2936
|
+
code: `${propName}: ...`
|
|
1578
2937
|
});
|
|
1579
2938
|
}
|
|
1580
2939
|
}
|
|
1581
2940
|
}
|
|
2941
|
+
// Check that at least one required property is present
|
|
2942
|
+
if (!hasViewID && !hasViewName && !hasViewEntity && !hasEntityName) {
|
|
2943
|
+
violations.push({
|
|
2944
|
+
rule: 'runview-runquery-valid-properties',
|
|
2945
|
+
severity: 'critical',
|
|
2946
|
+
line: config.loc?.start.line || 0,
|
|
2947
|
+
column: config.loc?.start.column || 0,
|
|
2948
|
+
message: `${methodName} requires one of: ViewID, ViewName, ViewEntity, or EntityName. Add one to identify what data to retrieve.`,
|
|
2949
|
+
code: `${methodName}({ ... })`
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
1582
2952
|
}
|
|
1583
2953
|
}
|
|
1584
2954
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
2955
|
+
// Check for utilities.rq.RunQuery
|
|
2956
|
+
if (t.isMemberExpression(callee) &&
|
|
2957
|
+
t.isMemberExpression(callee.object) &&
|
|
2958
|
+
t.isIdentifier(callee.object.object) &&
|
|
2959
|
+
callee.object.object.name === 'utilities' &&
|
|
2960
|
+
t.isIdentifier(callee.object.property) &&
|
|
2961
|
+
callee.object.property.name === 'rq' &&
|
|
2962
|
+
t.isIdentifier(callee.property) &&
|
|
2963
|
+
callee.property.name === 'RunQuery') {
|
|
2964
|
+
// Check that first parameter exists and is an object
|
|
2965
|
+
if (!path.node.arguments[0]) {
|
|
2966
|
+
violations.push({
|
|
2967
|
+
rule: 'runview-runquery-valid-properties',
|
|
2968
|
+
severity: 'critical',
|
|
2969
|
+
line: path.node.loc?.start.line || 0,
|
|
2970
|
+
column: path.node.loc?.start.column || 0,
|
|
2971
|
+
message: `RunQuery requires a RunQueryParams object as the first parameter. Must provide an object with either QueryID or QueryName.`,
|
|
2972
|
+
code: `RunQuery()`
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
else if (!t.isObjectExpression(path.node.arguments[0])) {
|
|
2976
|
+
// First parameter is not an object
|
|
2977
|
+
const argType = t.isStringLiteral(path.node.arguments[0]) ? 'string' :
|
|
2978
|
+
t.isIdentifier(path.node.arguments[0]) ? 'identifier' :
|
|
2979
|
+
'non-object';
|
|
2980
|
+
violations.push({
|
|
2981
|
+
rule: 'runview-runquery-valid-properties',
|
|
2982
|
+
severity: 'critical',
|
|
2983
|
+
line: path.node.arguments[0].loc?.start.line || 0,
|
|
2984
|
+
column: path.node.arguments[0].loc?.start.column || 0,
|
|
2985
|
+
message: `RunQuery expects a RunQueryParams object, not a ${argType}. Use: RunQuery({ QueryName: 'YourQuery' }) or RunQuery({ QueryID: 'id' })`,
|
|
2986
|
+
code: path.toString().substring(0, 100)
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
else {
|
|
2990
|
+
const config = path.node.arguments[0];
|
|
2991
|
+
// Check for required properties (must have QueryID or QueryName)
|
|
2992
|
+
let hasQueryID = false;
|
|
2993
|
+
let hasQueryName = false;
|
|
2994
|
+
for (const prop of config.properties) {
|
|
2995
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
2996
|
+
const propName = prop.key.name;
|
|
2997
|
+
if (propName === 'QueryID')
|
|
2998
|
+
hasQueryID = true;
|
|
2999
|
+
if (propName === 'QueryName')
|
|
3000
|
+
hasQueryName = true;
|
|
3001
|
+
if (!validRunQueryProps.has(propName)) {
|
|
3002
|
+
let message = `Invalid property '${propName}' on RunQuery. Valid properties: ${Array.from(validRunQueryProps).join(', ')}`;
|
|
3003
|
+
let fix = `Remove '${propName}' property`;
|
|
3004
|
+
if (propName === 'ExtraFilter') {
|
|
3005
|
+
message = `RunQuery does not support 'ExtraFilter'. WHERE clauses should be in the pre-defined query or passed as Parameters.`;
|
|
3006
|
+
fix = `Remove 'ExtraFilter'. Add WHERE logic to the query definition or pass as Parameters`;
|
|
3007
|
+
}
|
|
3008
|
+
else if (propName === 'Fields') {
|
|
3009
|
+
message = `RunQuery does not support 'Fields'. The query definition determines returned fields.`;
|
|
3010
|
+
fix = `Remove 'Fields'. Modify the query definition to return desired fields`;
|
|
3011
|
+
}
|
|
3012
|
+
else if (propName === 'OrderBy') {
|
|
3013
|
+
message = `RunQuery does not support 'OrderBy'. ORDER BY should be in the query definition.`;
|
|
3014
|
+
fix = `Remove 'OrderBy'. Add ORDER BY to the query definition`;
|
|
3015
|
+
}
|
|
3016
|
+
violations.push({
|
|
3017
|
+
rule: 'runview-runquery-valid-properties',
|
|
3018
|
+
severity: 'critical',
|
|
3019
|
+
line: prop.loc?.start.line || 0,
|
|
3020
|
+
column: prop.loc?.start.column || 0,
|
|
3021
|
+
message,
|
|
3022
|
+
code: `${propName}: ...`
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
// Check that at least one required property is present
|
|
3028
|
+
if (!hasQueryID && !hasQueryName) {
|
|
3029
|
+
violations.push({
|
|
3030
|
+
rule: 'runview-runquery-valid-properties',
|
|
3031
|
+
severity: 'critical',
|
|
3032
|
+
line: config.loc?.start.line || 0,
|
|
3033
|
+
column: config.loc?.start.column || 0,
|
|
3034
|
+
message: `RunQuery requires either QueryID or QueryName property. Add one of these to identify the query to run.`,
|
|
3035
|
+
code: `RunQuery({ ... })`
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
1593
3038
|
}
|
|
1594
|
-
propertyAccesses.get(objName).add(propName);
|
|
1595
3039
|
}
|
|
1596
3040
|
}
|
|
1597
3041
|
});
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
3042
|
+
return violations;
|
|
3043
|
+
}
|
|
3044
|
+
},
|
|
3045
|
+
{
|
|
3046
|
+
name: 'root-component-props-restriction',
|
|
3047
|
+
appliesTo: 'root',
|
|
3048
|
+
test: (ast, componentName, componentSpec) => {
|
|
3049
|
+
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
|
+
(0, traverse_1.default)(ast, {
|
|
3055
|
+
FunctionDeclaration(path) {
|
|
3056
|
+
if (path.node.id && path.node.id.name === componentName && path.node.params[0]) {
|
|
3057
|
+
const param = path.node.params[0];
|
|
3058
|
+
if (t.isObjectPattern(param)) {
|
|
3059
|
+
const invalidProps = [];
|
|
3060
|
+
const allProps = [];
|
|
3061
|
+
for (const prop of param.properties) {
|
|
3062
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
3063
|
+
const propName = prop.key.name;
|
|
3064
|
+
allProps.push(propName);
|
|
3065
|
+
if (!standardProps.has(propName)) {
|
|
3066
|
+
invalidProps.push(propName);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
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) {
|
|
1610
3073
|
violations.push({
|
|
1611
|
-
rule: '
|
|
3074
|
+
rule: 'root-component-props-restriction',
|
|
1612
3075
|
severity: 'critical',
|
|
1613
|
-
line:
|
|
1614
|
-
column:
|
|
1615
|
-
message: `
|
|
1616
|
-
code: `Transform uses '${Array.from(transformation.transformedProps).join(', ')}' but code accesses '${accessedProp}'`
|
|
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().`
|
|
1617
3079
|
});
|
|
1618
3080
|
}
|
|
1619
3081
|
}
|
|
1620
3082
|
}
|
|
3083
|
+
},
|
|
3084
|
+
// Also check arrow function components
|
|
3085
|
+
VariableDeclarator(path) {
|
|
3086
|
+
if (t.isIdentifier(path.node.id) && path.node.id.name === componentName) {
|
|
3087
|
+
const init = path.node.init;
|
|
3088
|
+
if (t.isArrowFunctionExpression(init) && init.params[0]) {
|
|
3089
|
+
const param = init.params[0];
|
|
3090
|
+
if (t.isObjectPattern(param)) {
|
|
3091
|
+
const invalidProps = [];
|
|
3092
|
+
const allProps = [];
|
|
3093
|
+
for (const prop of param.properties) {
|
|
3094
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
3095
|
+
const propName = prop.key.name;
|
|
3096
|
+
allProps.push(propName);
|
|
3097
|
+
if (!standardProps.has(propName)) {
|
|
3098
|
+
invalidProps.push(propName);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
if (invalidProps.length > 0) {
|
|
3103
|
+
violations.push({
|
|
3104
|
+
rule: 'root-component-props-restriction',
|
|
3105
|
+
severity: 'critical',
|
|
3106
|
+
line: path.node.loc?.start.line || 0,
|
|
3107
|
+
column: path.node.loc?.start.column || 0,
|
|
3108
|
+
message: `Component "${componentName}" accepts non-standard props: ${invalidProps.join(', ')}. Root components can only accept standard props: ${Array.from(standardProps).join(', ')}. Load data internally using utilities.rv.RunView().`
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
1621
3114
|
}
|
|
1622
|
-
}
|
|
3115
|
+
});
|
|
1623
3116
|
return violations;
|
|
1624
3117
|
}
|
|
1625
3118
|
},
|
|
1626
|
-
// New rules to align with AI linter
|
|
1627
3119
|
{
|
|
1628
|
-
name: '
|
|
3120
|
+
name: 'invalid-components-destructuring',
|
|
1629
3121
|
appliesTo: 'all',
|
|
1630
3122
|
test: (ast, componentName, componentSpec) => {
|
|
1631
3123
|
const violations = [];
|
|
3124
|
+
// Build sets of valid component names and library names
|
|
3125
|
+
const validComponentNames = new Set();
|
|
3126
|
+
const libraryNames = new Set();
|
|
3127
|
+
const libraryGlobalVars = new Set();
|
|
3128
|
+
// Add dependency components
|
|
3129
|
+
if (componentSpec?.dependencies) {
|
|
3130
|
+
for (const dep of componentSpec.dependencies) {
|
|
3131
|
+
if (dep.name) {
|
|
3132
|
+
validComponentNames.add(dep.name);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
// Add libraries
|
|
3137
|
+
if (componentSpec?.libraries) {
|
|
3138
|
+
for (const lib of componentSpec.libraries) {
|
|
3139
|
+
if (lib.name) {
|
|
3140
|
+
libraryNames.add(lib.name);
|
|
3141
|
+
}
|
|
3142
|
+
if (lib.globalVariable) {
|
|
3143
|
+
libraryGlobalVars.add(lib.globalVariable);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
// Check for invalid destructuring from components
|
|
1632
3148
|
(0, traverse_1.default)(ast, {
|
|
1633
|
-
|
|
1634
|
-
//
|
|
1635
|
-
if (t.
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
if (
|
|
1643
|
-
// Check if it
|
|
1644
|
-
|
|
1645
|
-
const hasDebounce = parentBody && parentBody.toString().includes('debounce');
|
|
1646
|
-
const hasTimeout = parentBody && parentBody.toString().includes('setTimeout');
|
|
1647
|
-
if (!hasDebounce && !hasTimeout) {
|
|
3149
|
+
VariableDeclarator(path) {
|
|
3150
|
+
// Look for: const { Something } = components;
|
|
3151
|
+
if (t.isObjectPattern(path.node.id) &&
|
|
3152
|
+
t.isIdentifier(path.node.init) &&
|
|
3153
|
+
path.node.init.name === 'components') {
|
|
3154
|
+
for (const prop of path.node.id.properties) {
|
|
3155
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
3156
|
+
const destructuredName = prop.key.name;
|
|
3157
|
+
// Check if this is NOT a valid component from dependencies
|
|
3158
|
+
if (!validComponentNames.has(destructuredName)) {
|
|
3159
|
+
// Check if it might be a library being incorrectly destructured
|
|
3160
|
+
if (libraryNames.has(destructuredName) || libraryGlobalVars.has(destructuredName)) {
|
|
1648
3161
|
violations.push({
|
|
1649
|
-
rule: '
|
|
3162
|
+
rule: 'invalid-components-destructuring',
|
|
1650
3163
|
severity: 'critical',
|
|
1651
|
-
line:
|
|
1652
|
-
column:
|
|
1653
|
-
message: `
|
|
3164
|
+
line: prop.loc?.start.line || 0,
|
|
3165
|
+
column: prop.loc?.start.column || 0,
|
|
3166
|
+
message: `Attempting to destructure library "${destructuredName}" from components prop. Libraries should be accessed directly via their globalVariable, not from components.`,
|
|
3167
|
+
code: `const { ${destructuredName} } = components;`
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
else {
|
|
3171
|
+
violations.push({
|
|
3172
|
+
rule: 'invalid-components-destructuring',
|
|
3173
|
+
severity: 'high',
|
|
3174
|
+
line: prop.loc?.start.line || 0,
|
|
3175
|
+
column: prop.loc?.start.column || 0,
|
|
3176
|
+
message: `Destructuring "${destructuredName}" from components prop, but it's not in the component's dependencies array. Either add it to dependencies or it might be a missing library.`,
|
|
3177
|
+
code: `const { ${destructuredName} } = components;`
|
|
1654
3178
|
});
|
|
1655
3179
|
}
|
|
1656
3180
|
}
|
|
@@ -1663,109 +3187,380 @@ ComponentLinter.universalComponentRules = [
|
|
|
1663
3187
|
}
|
|
1664
3188
|
},
|
|
1665
3189
|
{
|
|
1666
|
-
name: '
|
|
3190
|
+
name: 'unsafe-array-operations',
|
|
1667
3191
|
appliesTo: 'all',
|
|
1668
3192
|
test: (ast, componentName, componentSpec) => {
|
|
1669
3193
|
const violations = [];
|
|
3194
|
+
// Track which parameters are from props (likely from queries/RunView)
|
|
3195
|
+
const propsParams = new Set();
|
|
1670
3196
|
(0, traverse_1.default)(ast, {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
3197
|
+
// Find the main component function to identify props
|
|
3198
|
+
FunctionDeclaration(path) {
|
|
3199
|
+
if (path.node.id?.name === componentName) {
|
|
3200
|
+
const params = path.node.params[0];
|
|
3201
|
+
if (t.isObjectPattern(params)) {
|
|
3202
|
+
params.properties.forEach(prop => {
|
|
3203
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
3204
|
+
propsParams.add(prop.key.name);
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
},
|
|
3210
|
+
FunctionExpression(path) {
|
|
3211
|
+
// Also check function expressions
|
|
3212
|
+
const parent = path.parent;
|
|
3213
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
3214
|
+
if (parent.id.name === componentName) {
|
|
3215
|
+
const params = path.node.params[0];
|
|
3216
|
+
if (t.isObjectPattern(params)) {
|
|
3217
|
+
params.properties.forEach(prop => {
|
|
3218
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
3219
|
+
propsParams.add(prop.key.name);
|
|
3220
|
+
}
|
|
1691
3221
|
});
|
|
1692
3222
|
}
|
|
1693
3223
|
}
|
|
1694
3224
|
}
|
|
3225
|
+
},
|
|
3226
|
+
// Check for unsafe array operations
|
|
3227
|
+
MemberExpression(path) {
|
|
3228
|
+
const { object, property } = path.node;
|
|
3229
|
+
// Check for array methods that will crash on undefined
|
|
3230
|
+
const unsafeArrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'length'];
|
|
3231
|
+
if (t.isIdentifier(property) && unsafeArrayMethods.includes(property.name)) {
|
|
3232
|
+
// Check if the object is a prop parameter
|
|
3233
|
+
if (t.isIdentifier(object) && propsParams.has(object.name)) {
|
|
3234
|
+
// Look for common data prop patterns
|
|
3235
|
+
const isDataProp = object.name.toLowerCase().includes('data') ||
|
|
3236
|
+
object.name.toLowerCase().includes('items') ||
|
|
3237
|
+
object.name.toLowerCase().includes('results') ||
|
|
3238
|
+
object.name.toLowerCase().includes('records') ||
|
|
3239
|
+
object.name.toLowerCase().includes('list') ||
|
|
3240
|
+
object.name.toLowerCase().includes('types') ||
|
|
3241
|
+
object.name.toLowerCase().includes('options');
|
|
3242
|
+
if (isDataProp || property.name === 'length') {
|
|
3243
|
+
// Check if there's a guard nearby (within the same function/block)
|
|
3244
|
+
let hasGuard = false;
|
|
3245
|
+
// Check for optional chaining (?.length, ?.map)
|
|
3246
|
+
if (path.node.optional) {
|
|
3247
|
+
hasGuard = true;
|
|
3248
|
+
}
|
|
3249
|
+
// Check for (data || []).map pattern
|
|
3250
|
+
const parent = path.parent;
|
|
3251
|
+
if (t.isMemberExpression(parent) && t.isLogicalExpression(parent.object)) {
|
|
3252
|
+
if (parent.object.operator === '||' && t.isIdentifier(parent.object.left)) {
|
|
3253
|
+
if (parent.object.left.name === object.name) {
|
|
3254
|
+
hasGuard = true;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
// Check for inline guards like: data && data.map(...)
|
|
3259
|
+
const grandParent = path.parentPath?.parent;
|
|
3260
|
+
if (t.isLogicalExpression(grandParent) && grandParent.operator === '&&') {
|
|
3261
|
+
if (t.isIdentifier(grandParent.left) && grandParent.left.name === object.name) {
|
|
3262
|
+
hasGuard = true;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
// Check for early return guards in the function
|
|
3266
|
+
// This is harder to detect perfectly, but we can look for common patterns
|
|
3267
|
+
const functionParent = path.getFunctionParent();
|
|
3268
|
+
if (functionParent && !hasGuard) {
|
|
3269
|
+
let hasEarlyReturn = false;
|
|
3270
|
+
// Look for if statements with returns that check our variable
|
|
3271
|
+
functionParent.traverse({
|
|
3272
|
+
IfStatement(ifPath) {
|
|
3273
|
+
// Skip if this if statement comes after our usage
|
|
3274
|
+
if (ifPath.node.loc && path.node.loc) {
|
|
3275
|
+
if (ifPath.node.loc.start.line > path.node.loc.start.line) {
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
const test = ifPath.node.test;
|
|
3280
|
+
let checksOurVariable = false;
|
|
3281
|
+
// Check if the test involves our variable
|
|
3282
|
+
if (t.isUnaryExpression(test) && test.operator === '!') {
|
|
3283
|
+
if (t.isIdentifier(test.argument) && test.argument.name === object.name) {
|
|
3284
|
+
checksOurVariable = true;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
if (t.isLogicalExpression(test)) {
|
|
3288
|
+
// Check for !data || !Array.isArray(data) pattern
|
|
3289
|
+
ifPath.traverse({
|
|
3290
|
+
Identifier(idPath) {
|
|
3291
|
+
if (idPath.node.name === object.name) {
|
|
3292
|
+
checksOurVariable = true;
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
// Check if the consequent has a return statement
|
|
3298
|
+
if (checksOurVariable) {
|
|
3299
|
+
const consequent = ifPath.node.consequent;
|
|
3300
|
+
if (t.isBlockStatement(consequent)) {
|
|
3301
|
+
for (const stmt of consequent.body) {
|
|
3302
|
+
if (t.isReturnStatement(stmt)) {
|
|
3303
|
+
hasEarlyReturn = true;
|
|
3304
|
+
break;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
else if (t.isReturnStatement(consequent)) {
|
|
3309
|
+
hasEarlyReturn = true;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
});
|
|
3314
|
+
if (hasEarlyReturn) {
|
|
3315
|
+
hasGuard = true;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
if (!hasGuard) {
|
|
3319
|
+
const methodName = property.name;
|
|
3320
|
+
const severity = methodName === 'length' ? 'high' : 'high';
|
|
3321
|
+
violations.push({
|
|
3322
|
+
rule: 'unsafe-array-operations',
|
|
3323
|
+
severity,
|
|
3324
|
+
line: path.node.loc?.start.line || 0,
|
|
3325
|
+
column: path.node.loc?.start.column || 0,
|
|
3326
|
+
message: `Unsafe operation "${object.name}.${methodName}" on prop that may be undefined. Props from queries/RunView can be null/undefined on initial render. Add a guard: if (!${object.name} || !Array.isArray(${object.name})) return <div>Loading...</div>; OR use: ${object.name}?.${methodName} or (${object.name} || []).${methodName}`,
|
|
3327
|
+
code: `${object.name}.${methodName}`
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
1695
3333
|
}
|
|
1696
3334
|
});
|
|
1697
3335
|
return violations;
|
|
1698
3336
|
}
|
|
1699
3337
|
},
|
|
1700
3338
|
{
|
|
1701
|
-
name: '
|
|
3339
|
+
name: 'undefined-jsx-component',
|
|
1702
3340
|
appliesTo: 'all',
|
|
1703
3341
|
test: (ast, componentName, componentSpec) => {
|
|
1704
3342
|
const violations = [];
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
(
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
3343
|
+
// Track what's available in scope
|
|
3344
|
+
const availableIdentifiers = new Set();
|
|
3345
|
+
const componentsFromProp = new Set();
|
|
3346
|
+
const libraryGlobalVars = new Set();
|
|
3347
|
+
// Add React hooks and built-ins
|
|
3348
|
+
availableIdentifiers.add('React');
|
|
3349
|
+
REACT_BUILT_INS.forEach(name => availableIdentifiers.add(name));
|
|
3350
|
+
availableIdentifiers.add('useState');
|
|
3351
|
+
availableIdentifiers.add('useEffect');
|
|
3352
|
+
availableIdentifiers.add('useCallback');
|
|
3353
|
+
availableIdentifiers.add('useMemo');
|
|
3354
|
+
availableIdentifiers.add('useRef');
|
|
3355
|
+
availableIdentifiers.add('useContext');
|
|
3356
|
+
availableIdentifiers.add('useReducer');
|
|
3357
|
+
availableIdentifiers.add('useLayoutEffect');
|
|
3358
|
+
// Add HTML elements from our comprehensive list
|
|
3359
|
+
HTML_ELEMENTS.forEach(el => availableIdentifiers.add(el));
|
|
3360
|
+
// Add library global variables
|
|
3361
|
+
if (componentSpec?.libraries) {
|
|
3362
|
+
for (const lib of componentSpec.libraries) {
|
|
3363
|
+
if (lib.globalVariable) {
|
|
3364
|
+
libraryGlobalVars.add(lib.globalVariable);
|
|
3365
|
+
availableIdentifiers.add(lib.globalVariable);
|
|
1714
3366
|
}
|
|
1715
3367
|
}
|
|
1716
|
-
}
|
|
1717
|
-
//
|
|
3368
|
+
}
|
|
3369
|
+
// Track what's destructured from components
|
|
3370
|
+
if (componentSpec?.dependencies) {
|
|
3371
|
+
for (const dep of componentSpec.dependencies) {
|
|
3372
|
+
if (dep.name) {
|
|
3373
|
+
componentsFromProp.add(dep.name);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
1718
3377
|
(0, traverse_1.default)(ast, {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
3378
|
+
// Track variable declarations
|
|
3379
|
+
VariableDeclarator(path) {
|
|
3380
|
+
if (t.isIdentifier(path.node.id)) {
|
|
3381
|
+
availableIdentifiers.add(path.node.id.name);
|
|
3382
|
+
}
|
|
3383
|
+
else if (t.isObjectPattern(path.node.id)) {
|
|
3384
|
+
// Track destructured variables
|
|
3385
|
+
for (const prop of path.node.id.properties) {
|
|
3386
|
+
if (t.isObjectProperty(prop)) {
|
|
3387
|
+
if (t.isIdentifier(prop.value)) {
|
|
3388
|
+
availableIdentifiers.add(prop.value.name);
|
|
3389
|
+
}
|
|
3390
|
+
else if (t.isIdentifier(prop.key)) {
|
|
3391
|
+
availableIdentifiers.add(prop.key.name);
|
|
3392
|
+
}
|
|
1728
3393
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
},
|
|
3397
|
+
// Track function declarations
|
|
3398
|
+
FunctionDeclaration(path) {
|
|
3399
|
+
if (path.node.id) {
|
|
3400
|
+
availableIdentifiers.add(path.node.id.name);
|
|
3401
|
+
}
|
|
3402
|
+
},
|
|
3403
|
+
// Check JSX elements
|
|
3404
|
+
JSXElement(path) {
|
|
3405
|
+
const openingElement = path.node.openingElement;
|
|
3406
|
+
// Handle JSXMemberExpression (e.g., <library.Component>)
|
|
3407
|
+
if (t.isJSXMemberExpression(openingElement.name)) {
|
|
3408
|
+
let objectName = '';
|
|
3409
|
+
if (t.isJSXIdentifier(openingElement.name.object)) {
|
|
3410
|
+
objectName = openingElement.name.object.name;
|
|
3411
|
+
}
|
|
3412
|
+
// Check if the object (library global) is available
|
|
3413
|
+
if (objectName && !availableIdentifiers.has(objectName)) {
|
|
3414
|
+
// Check if it looks like a library global that should exist
|
|
3415
|
+
const isLikelyLibrary = /^[a-z][a-zA-Z]*$/.test(objectName) || // camelCase like agGrid
|
|
3416
|
+
/^[A-Z][a-zA-Z]*$/.test(objectName); // PascalCase like MaterialUI
|
|
3417
|
+
if (isLikelyLibrary) {
|
|
3418
|
+
// Suggest available library globals
|
|
3419
|
+
const availableLibraries = Array.from(libraryGlobalVars);
|
|
3420
|
+
if (availableLibraries.length > 0) {
|
|
3421
|
+
// Try to find a close match
|
|
3422
|
+
let suggestion = '';
|
|
3423
|
+
for (const lib of availableLibraries) {
|
|
3424
|
+
// Check for common patterns like agGridReact -> agGrid
|
|
3425
|
+
if (objectName.toLowerCase().includes(lib.toLowerCase().replace('grid', '')) ||
|
|
3426
|
+
lib.toLowerCase().includes(objectName.toLowerCase().replace('react', ''))) {
|
|
3427
|
+
suggestion = lib;
|
|
3428
|
+
break;
|
|
1746
3429
|
}
|
|
1747
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 {
|
|
3442
|
+
violations.push({
|
|
3443
|
+
rule: 'undefined-jsx-component',
|
|
3444
|
+
severity: 'critical',
|
|
3445
|
+
line: openingElement.loc?.start.line || 0,
|
|
3446
|
+
column: openingElement.loc?.start.column || 0,
|
|
3447
|
+
message: `Library global "${objectName}" is not defined. Available library globals: ${availableLibraries.join(', ')}`,
|
|
3448
|
+
code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
else {
|
|
3453
|
+
violations.push({
|
|
3454
|
+
rule: 'undefined-jsx-component',
|
|
3455
|
+
severity: 'critical',
|
|
3456
|
+
line: openingElement.loc?.start.line || 0,
|
|
3457
|
+
column: openingElement.loc?.start.column || 0,
|
|
3458
|
+
message: `"${objectName}" is not defined. It appears to be a library global, but no libraries are specified in the component specification.`,
|
|
3459
|
+
code: `<${objectName}.${t.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : '...'} />`
|
|
3460
|
+
});
|
|
1748
3461
|
}
|
|
1749
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
|
+
}
|
|
1750
3474
|
}
|
|
3475
|
+
return; // Done with member expression
|
|
1751
3476
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
(
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
|
3522
|
+
violations.push({
|
|
3523
|
+
rule: 'undefined-jsx-component',
|
|
3524
|
+
severity: 'critical',
|
|
3525
|
+
line: openingElement.loc?.start.line || 0,
|
|
3526
|
+
column: openingElement.loc?.start.column || 0,
|
|
3527
|
+
message: `JSX component "${tagName}" is not defined. This appears to be a library component, but no libraries have been specified in the component specification. The use of external libraries has not been authorized for this component. Components without library specifications cannot use external libraries.`,
|
|
3528
|
+
code: `<${tagName} ... />`
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
else if (componentsFromProp.has(tagName)) {
|
|
3533
|
+
// It's a component from the components prop
|
|
3534
|
+
violations.push({
|
|
3535
|
+
rule: 'undefined-jsx-component',
|
|
3536
|
+
severity: 'high',
|
|
3537
|
+
line: openingElement.loc?.start.line || 0,
|
|
3538
|
+
column: openingElement.loc?.start.column || 0,
|
|
3539
|
+
message: `JSX component "${tagName}" is in dependencies but not destructured from components prop. Add: const { ${tagName} } = components;`,
|
|
3540
|
+
code: `<${tagName} ... />`
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
else {
|
|
3544
|
+
// Unknown component - not in libraries, not in dependencies
|
|
3545
|
+
violations.push({
|
|
3546
|
+
rule: 'undefined-jsx-component',
|
|
3547
|
+
severity: 'high',
|
|
3548
|
+
line: openingElement.loc?.start.line || 0,
|
|
3549
|
+
column: openingElement.loc?.start.column || 0,
|
|
3550
|
+
message: `JSX component "${tagName}" is not defined. Either define it in your component, add it to dependencies, or check if it should be destructured from a library.`,
|
|
3551
|
+
code: `<${tagName} ... />`
|
|
3552
|
+
});
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
else {
|
|
3556
|
+
// Not PascalCase but also not a built-in - suspicious
|
|
1762
3557
|
violations.push({
|
|
1763
|
-
rule: '
|
|
1764
|
-
severity: '
|
|
1765
|
-
line:
|
|
1766
|
-
column:
|
|
1767
|
-
message:
|
|
1768
|
-
code:
|
|
3558
|
+
rule: 'undefined-jsx-component',
|
|
3559
|
+
severity: 'medium',
|
|
3560
|
+
line: openingElement.loc?.start.line || 0,
|
|
3561
|
+
column: openingElement.loc?.start.column || 0,
|
|
3562
|
+
message: `JSX element "${tagName}" is not recognized as a valid HTML element or React component. Check the spelling or ensure it's properly defined.`,
|
|
3563
|
+
code: `<${tagName} ... />`
|
|
1769
3564
|
});
|
|
1770
3565
|
}
|
|
1771
3566
|
}
|
|
@@ -1776,34 +3571,42 @@ ComponentLinter.universalComponentRules = [
|
|
|
1776
3571
|
}
|
|
1777
3572
|
},
|
|
1778
3573
|
{
|
|
1779
|
-
name: '
|
|
3574
|
+
name: 'runview-runquery-result-direct-usage',
|
|
1780
3575
|
appliesTo: 'all',
|
|
1781
3576
|
test: (ast, componentName, componentSpec) => {
|
|
1782
3577
|
const violations = [];
|
|
3578
|
+
// Track variables that hold RunView/RunQuery results
|
|
3579
|
+
const resultVariables = new Map();
|
|
1783
3580
|
(0, traverse_1.default)(ast, {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
if (
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
3581
|
+
// First pass: identify RunView/RunQuery calls and their assigned variables
|
|
3582
|
+
AwaitExpression(path) {
|
|
3583
|
+
const callExpr = path.node.argument;
|
|
3584
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
3585
|
+
const callee = callExpr.callee;
|
|
3586
|
+
// Check for utilities.rv.RunView or utilities.rq.RunQuery pattern
|
|
3587
|
+
if (t.isMemberExpression(callee.object) &&
|
|
3588
|
+
t.isIdentifier(callee.object.object) &&
|
|
3589
|
+
callee.object.object.name === 'utilities') {
|
|
3590
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
3591
|
+
const isRunView = method === 'RunView' || method === 'RunViews';
|
|
3592
|
+
const isRunQuery = method === 'RunQuery';
|
|
3593
|
+
if (isRunView || isRunQuery) {
|
|
3594
|
+
// Check if this is being assigned to a variable
|
|
3595
|
+
const parent = path.parent;
|
|
3596
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
3597
|
+
// const result = await utilities.rv.RunView(...)
|
|
3598
|
+
resultVariables.set(parent.id.name, {
|
|
3599
|
+
line: parent.id.loc?.start.line || 0,
|
|
3600
|
+
column: parent.id.loc?.start.column || 0,
|
|
3601
|
+
method: isRunView ? 'RunView' : 'RunQuery'
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
3605
|
+
// result = await utilities.rv.RunView(...)
|
|
3606
|
+
resultVariables.set(parent.left.name, {
|
|
3607
|
+
line: parent.left.loc?.start.line || 0,
|
|
3608
|
+
column: parent.left.loc?.start.column || 0,
|
|
3609
|
+
method: isRunView ? 'RunView' : 'RunQuery'
|
|
1807
3610
|
});
|
|
1808
3611
|
}
|
|
1809
3612
|
}
|
|
@@ -1811,34 +3614,84 @@ ComponentLinter.universalComponentRules = [
|
|
|
1811
3614
|
}
|
|
1812
3615
|
}
|
|
1813
3616
|
});
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1816
|
-
},
|
|
1817
|
-
{
|
|
1818
|
-
name: 'server-reload-on-client-operation',
|
|
1819
|
-
appliesTo: 'all',
|
|
1820
|
-
test: (ast, componentName, componentSpec) => {
|
|
1821
|
-
const violations = [];
|
|
3617
|
+
// Second pass: check for misuse of these result variables
|
|
1822
3618
|
(0, traverse_1.default)(ast, {
|
|
3619
|
+
// Check for direct array operations
|
|
1823
3620
|
CallExpression(path) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
(
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
3621
|
+
// Check for array methods being called on result objects
|
|
3622
|
+
if (t.isMemberExpression(path.node.callee) &&
|
|
3623
|
+
t.isIdentifier(path.node.callee.object) &&
|
|
3624
|
+
t.isIdentifier(path.node.callee.property)) {
|
|
3625
|
+
const objName = path.node.callee.object.name;
|
|
3626
|
+
const methodName = path.node.callee.property.name;
|
|
3627
|
+
// Array methods that would fail on a result object
|
|
3628
|
+
const arrayMethods = ['map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'sort', 'concat'];
|
|
3629
|
+
if (resultVariables.has(objName) && arrayMethods.includes(methodName)) {
|
|
3630
|
+
const resultInfo = resultVariables.get(objName);
|
|
3631
|
+
violations.push({
|
|
3632
|
+
rule: 'runview-runquery-result-direct-usage',
|
|
3633
|
+
severity: 'critical',
|
|
3634
|
+
line: path.node.loc?.start.line || 0,
|
|
3635
|
+
column: path.node.loc?.start.column || 0,
|
|
3636
|
+
message: `Cannot call array method "${methodName}" directly on ${resultInfo.method} result. Use "${objName}.Results.${methodName}(...)" instead. ${resultInfo.method} returns an object with { Success, Results, ... }, not an array.`,
|
|
3637
|
+
code: `${objName}.${methodName}(...)`
|
|
3638
|
+
});
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
},
|
|
3642
|
+
// Check for direct usage in setState or as function arguments
|
|
3643
|
+
Identifier(path) {
|
|
3644
|
+
const varName = path.node.name;
|
|
3645
|
+
if (resultVariables.has(varName)) {
|
|
3646
|
+
const resultInfo = resultVariables.get(varName);
|
|
3647
|
+
const parent = path.parent;
|
|
3648
|
+
// Check if being passed to setState-like functions
|
|
3649
|
+
if (t.isCallExpression(parent) && path.node === parent.arguments[0]) {
|
|
3650
|
+
const callee = parent.callee;
|
|
3651
|
+
// Check for setState patterns
|
|
3652
|
+
if (t.isIdentifier(callee) && /^set[A-Z]/.test(callee.name)) {
|
|
3653
|
+
// Likely a setState function
|
|
1835
3654
|
violations.push({
|
|
1836
|
-
rule: '
|
|
3655
|
+
rule: 'runview-runquery-result-direct-usage',
|
|
1837
3656
|
severity: 'critical',
|
|
1838
3657
|
line: path.node.loc?.start.line || 0,
|
|
1839
3658
|
column: path.node.loc?.start.column || 0,
|
|
1840
|
-
message:
|
|
1841
|
-
code: `${
|
|
3659
|
+
message: `Passing ${resultInfo.method} result directly to setState. Use "${varName}.Results" or check "${varName}.Success" first. ${resultInfo.method} returns { Success, Results, ErrorMessage }, not the data array.`,
|
|
3660
|
+
code: `${callee.name}(${varName})`
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
// Check for array-expecting functions
|
|
3664
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
3665
|
+
const methodName = callee.property.name;
|
|
3666
|
+
if (methodName === 'concat' || methodName === 'push' || methodName === 'unshift') {
|
|
3667
|
+
violations.push({
|
|
3668
|
+
rule: 'runview-runquery-result-direct-usage',
|
|
3669
|
+
severity: 'critical',
|
|
3670
|
+
line: path.node.loc?.start.line || 0,
|
|
3671
|
+
column: path.node.loc?.start.column || 0,
|
|
3672
|
+
message: `Passing ${resultInfo.method} result to array method. Use "${varName}.Results" instead of "${varName}".`,
|
|
3673
|
+
code: `...${methodName}(${varName})`
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
// Check for ternary with Array.isArray check (common pattern)
|
|
3679
|
+
if (t.isConditionalExpression(parent) &&
|
|
3680
|
+
t.isCallExpression(parent.test) &&
|
|
3681
|
+
t.isMemberExpression(parent.test.callee) &&
|
|
3682
|
+
t.isIdentifier(parent.test.callee.object) &&
|
|
3683
|
+
parent.test.callee.object.name === 'Array' &&
|
|
3684
|
+
t.isIdentifier(parent.test.callee.property) &&
|
|
3685
|
+
parent.test.callee.property.name === 'isArray') {
|
|
3686
|
+
// Pattern: Array.isArray(result) ? result : []
|
|
3687
|
+
if (parent.test.arguments[0] === path.node && parent.consequent === path.node) {
|
|
3688
|
+
violations.push({
|
|
3689
|
+
rule: 'runview-runquery-result-direct-usage',
|
|
3690
|
+
severity: 'high',
|
|
3691
|
+
line: path.node.loc?.start.line || 0,
|
|
3692
|
+
column: path.node.loc?.start.column || 0,
|
|
3693
|
+
message: `${resultInfo.method} result is never an array. Use "${varName}.Results || []" instead of "Array.isArray(${varName}) ? ${varName} : []".`,
|
|
3694
|
+
code: `Array.isArray(${varName}) ? ${varName} : []`
|
|
1842
3695
|
});
|
|
1843
3696
|
}
|
|
1844
3697
|
}
|
|
@@ -1849,194 +3702,298 @@ ComponentLinter.universalComponentRules = [
|
|
|
1849
3702
|
}
|
|
1850
3703
|
},
|
|
1851
3704
|
{
|
|
1852
|
-
name: 'runview-
|
|
3705
|
+
name: 'runquery-runview-result-structure',
|
|
1853
3706
|
appliesTo: 'all',
|
|
1854
3707
|
test: (ast, componentName, componentSpec) => {
|
|
1855
3708
|
const violations = [];
|
|
1856
|
-
// Valid properties for
|
|
1857
|
-
const
|
|
1858
|
-
'
|
|
1859
|
-
'
|
|
1860
|
-
'ResultType'
|
|
3709
|
+
// Valid properties for RunQueryResult
|
|
3710
|
+
const validRunQueryResultProps = new Set([
|
|
3711
|
+
'QueryID', 'QueryName', 'Success', 'Results', 'RowCount',
|
|
3712
|
+
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
1861
3713
|
]);
|
|
1862
|
-
// Valid properties for
|
|
1863
|
-
const
|
|
1864
|
-
'
|
|
3714
|
+
// Valid properties for RunViewResult
|
|
3715
|
+
const validRunViewResultProps = new Set([
|
|
3716
|
+
'Success', 'Results', 'UserViewRunID', 'RowCount',
|
|
3717
|
+
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
1865
3718
|
]);
|
|
3719
|
+
// Common incorrect patterns
|
|
3720
|
+
const invalidResultPatterns = new Set(['data', 'rows', 'records', 'items', 'values']);
|
|
1866
3721
|
(0, traverse_1.default)(ast, {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
if (
|
|
1879
|
-
//
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
3722
|
+
MemberExpression(path) {
|
|
3723
|
+
// Check if this is accessing a property on a variable that looks like a query/view result
|
|
3724
|
+
if (t.isIdentifier(path.node.object) && t.isIdentifier(path.node.property)) {
|
|
3725
|
+
const objName = path.node.object.name;
|
|
3726
|
+
const propName = path.node.property.name;
|
|
3727
|
+
// Check if the object name suggests it's a query or view result
|
|
3728
|
+
const isLikelyQueryResult = /result|response|res|data|output/i.test(objName);
|
|
3729
|
+
const isFromRunQuery = path.scope.hasBinding(objName) &&
|
|
3730
|
+
ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunQuery');
|
|
3731
|
+
const isFromRunView = path.scope.hasBinding(objName) &&
|
|
3732
|
+
ComponentLinter.isVariableFromRunQueryOrView(path, objName, 'RunView');
|
|
3733
|
+
if (isLikelyQueryResult || isFromRunQuery || isFromRunView) {
|
|
3734
|
+
// Check for common incorrect patterns
|
|
3735
|
+
if (invalidResultPatterns.has(propName)) {
|
|
3736
|
+
violations.push({
|
|
3737
|
+
rule: 'runquery-runview-result-structure',
|
|
3738
|
+
severity: 'high',
|
|
3739
|
+
line: path.node.loc?.start.line || 0,
|
|
3740
|
+
column: path.node.loc?.start.column || 0,
|
|
3741
|
+
message: `Incorrect property access "${objName}.${propName}". RunQuery/RunView results use ".Results" for data array, not ".${propName}". Change to "${objName}.Results"`,
|
|
3742
|
+
code: `${objName}.${propName}`
|
|
3743
|
+
});
|
|
1887
3744
|
}
|
|
1888
|
-
else if (
|
|
1889
|
-
//
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
3745
|
+
else if (propName === 'data') {
|
|
3746
|
+
// Special case for .data - very common mistake
|
|
3747
|
+
violations.push({
|
|
3748
|
+
rule: 'runquery-runview-result-structure',
|
|
3749
|
+
severity: 'critical',
|
|
3750
|
+
line: path.node.loc?.start.line || 0,
|
|
3751
|
+
column: path.node.loc?.start.column || 0,
|
|
3752
|
+
message: `RunQuery/RunView results don't have a ".data" property. Use ".Results" to access the array of returned rows. Change "${objName}.data" to "${objName}.Results"`,
|
|
3753
|
+
code: `${objName}.${propName}`
|
|
3754
|
+
});
|
|
1893
3755
|
}
|
|
1894
|
-
// Check
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
else if (propName === 'GroupBy') {
|
|
1908
|
-
message = `${methodName} does not support 'GroupBy'. Use RunQuery with a pre-defined query for aggregations.`;
|
|
1909
|
-
fix = `Remove 'GroupBy' and use RunQuery instead for aggregated data`;
|
|
1910
|
-
}
|
|
1911
|
-
else if (propName === 'Having') {
|
|
1912
|
-
message = `${methodName} does not support 'Having'. Use RunQuery with a pre-defined query.`;
|
|
1913
|
-
fix = `Remove 'Having' and use RunQuery instead`;
|
|
1914
|
-
}
|
|
1915
|
-
violations.push({
|
|
1916
|
-
rule: 'runview-runquery-valid-properties',
|
|
1917
|
-
severity: 'critical',
|
|
1918
|
-
line: prop.loc?.start.line || 0,
|
|
1919
|
-
column: prop.loc?.start.column || 0,
|
|
1920
|
-
message,
|
|
1921
|
-
code: `${propName}: ...`
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
3756
|
+
// Check for nested incorrect access like result.data.entities
|
|
3757
|
+
if (t.isMemberExpression(path.parent) &&
|
|
3758
|
+
t.isIdentifier(path.parent.property) &&
|
|
3759
|
+
propName === 'data') {
|
|
3760
|
+
const nestedProp = path.parent.property.name;
|
|
3761
|
+
violations.push({
|
|
3762
|
+
rule: 'runquery-runview-result-structure',
|
|
3763
|
+
severity: 'critical',
|
|
3764
|
+
line: path.parent.loc?.start.line || 0,
|
|
3765
|
+
column: path.parent.loc?.start.column || 0,
|
|
3766
|
+
message: `Incorrect nested property access "${objName}.data.${nestedProp}". RunQuery/RunView results use ".Results" directly for the data array. Change to "${objName}.Results"`,
|
|
3767
|
+
code: `${objName}.data.${nestedProp}`
|
|
3768
|
+
});
|
|
1926
3769
|
}
|
|
1927
3770
|
}
|
|
1928
3771
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
if (path.node.arguments[0] && t.isObjectExpression(path.node.arguments[0])) {
|
|
1939
|
-
const config = path.node.arguments[0];
|
|
1940
|
-
for (const prop of config.properties) {
|
|
3772
|
+
},
|
|
3773
|
+
// Check for destructuring patterns
|
|
3774
|
+
VariableDeclarator(path) {
|
|
3775
|
+
// Check for destructuring from a result object
|
|
3776
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
3777
|
+
const sourceName = path.node.init.name;
|
|
3778
|
+
// Check if this looks like destructuring from a query/view result
|
|
3779
|
+
if (/result|response|res/i.test(sourceName)) {
|
|
3780
|
+
for (const prop of path.node.id.properties) {
|
|
1941
3781
|
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
1942
3782
|
const propName = prop.key.name;
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
let fix = `Remove '${propName}' property`;
|
|
1946
|
-
if (propName === 'ExtraFilter') {
|
|
1947
|
-
message = `RunQuery does not support 'ExtraFilter'. WHERE clauses should be in the pre-defined query or passed as Parameters.`;
|
|
1948
|
-
fix = `Remove 'ExtraFilter'. Add WHERE logic to the query definition or pass as Parameters`;
|
|
1949
|
-
}
|
|
1950
|
-
else if (propName === 'Fields') {
|
|
1951
|
-
message = `RunQuery does not support 'Fields'. The query definition determines returned fields.`;
|
|
1952
|
-
fix = `Remove 'Fields'. Modify the query definition to return desired fields`;
|
|
1953
|
-
}
|
|
1954
|
-
else if (propName === 'OrderBy') {
|
|
1955
|
-
message = `RunQuery does not support 'OrderBy'. ORDER BY should be in the query definition.`;
|
|
1956
|
-
fix = `Remove 'OrderBy'. Add ORDER BY to the query definition`;
|
|
1957
|
-
}
|
|
3783
|
+
// Check for incorrect destructuring
|
|
3784
|
+
if (propName === 'data') {
|
|
1958
3785
|
violations.push({
|
|
1959
|
-
rule: 'runview-
|
|
3786
|
+
rule: 'runquery-runview-result-structure',
|
|
1960
3787
|
severity: 'critical',
|
|
1961
3788
|
line: prop.loc?.start.line || 0,
|
|
1962
3789
|
column: prop.loc?.start.column || 0,
|
|
1963
|
-
message,
|
|
1964
|
-
code:
|
|
3790
|
+
message: `Destructuring "data" from RunQuery/RunView result. The property is named "Results", not "data". Change "const { data } = ${sourceName}" to "const { Results } = ${sourceName}"`,
|
|
3791
|
+
code: `{ data }`
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
else if (invalidResultPatterns.has(propName) && propName !== 'data') {
|
|
3795
|
+
violations.push({
|
|
3796
|
+
rule: 'runquery-runview-result-structure',
|
|
3797
|
+
severity: 'medium',
|
|
3798
|
+
line: prop.loc?.start.line || 0,
|
|
3799
|
+
column: prop.loc?.start.column || 0,
|
|
3800
|
+
message: `Destructuring "${propName}" from what appears to be a RunQuery/RunView result. Did you mean "Results"?`,
|
|
3801
|
+
code: `{ ${propName} }`
|
|
1965
3802
|
});
|
|
1966
3803
|
}
|
|
1967
3804
|
}
|
|
1968
3805
|
}
|
|
1969
3806
|
}
|
|
1970
3807
|
}
|
|
3808
|
+
},
|
|
3809
|
+
// Check for conditional access without checking Success
|
|
3810
|
+
IfStatement(path) {
|
|
3811
|
+
const test = path.node.test;
|
|
3812
|
+
// Look for patterns like: if (result) or if (result.Results) without checking Success
|
|
3813
|
+
if (t.isIdentifier(test) ||
|
|
3814
|
+
(t.isMemberExpression(test) &&
|
|
3815
|
+
t.isIdentifier(test.object) &&
|
|
3816
|
+
t.isIdentifier(test.property) &&
|
|
3817
|
+
test.property.name === 'Results')) {
|
|
3818
|
+
let varName = '';
|
|
3819
|
+
if (t.isIdentifier(test)) {
|
|
3820
|
+
varName = test.name;
|
|
3821
|
+
}
|
|
3822
|
+
else if (t.isMemberExpression(test) && t.isIdentifier(test.object)) {
|
|
3823
|
+
varName = test.object.name;
|
|
3824
|
+
}
|
|
3825
|
+
// Check if this variable is from RunQuery/RunView
|
|
3826
|
+
if (/result|response|res/i.test(varName)) {
|
|
3827
|
+
// Look for .Results access in the consequent without .Success check
|
|
3828
|
+
let hasResultsAccess = false;
|
|
3829
|
+
let hasSuccessCheck = false;
|
|
3830
|
+
(0, traverse_1.default)(path.node, {
|
|
3831
|
+
MemberExpression(innerPath) {
|
|
3832
|
+
if (t.isIdentifier(innerPath.node.object) &&
|
|
3833
|
+
innerPath.node.object.name === varName) {
|
|
3834
|
+
if (t.isIdentifier(innerPath.node.property)) {
|
|
3835
|
+
if (innerPath.node.property.name === 'Results') {
|
|
3836
|
+
hasResultsAccess = true;
|
|
3837
|
+
}
|
|
3838
|
+
if (innerPath.node.property.name === 'Success') {
|
|
3839
|
+
hasSuccessCheck = true;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
}, path.scope);
|
|
3845
|
+
if (hasResultsAccess && !hasSuccessCheck) {
|
|
3846
|
+
violations.push({
|
|
3847
|
+
rule: 'runquery-runview-result-structure',
|
|
3848
|
+
severity: 'medium',
|
|
3849
|
+
line: test.loc?.start.line || 0,
|
|
3850
|
+
column: test.loc?.start.column || 0,
|
|
3851
|
+
message: `Accessing "${varName}.Results" without checking "${varName}.Success" first. Always verify Success before accessing Results.`,
|
|
3852
|
+
code: `if (${varName}) { ... ${varName}.Results ... }`
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
1971
3857
|
}
|
|
1972
3858
|
});
|
|
1973
3859
|
return violations;
|
|
1974
3860
|
}
|
|
1975
3861
|
},
|
|
1976
3862
|
{
|
|
1977
|
-
name: '
|
|
1978
|
-
appliesTo: '
|
|
3863
|
+
name: 'dependency-prop-validation',
|
|
3864
|
+
appliesTo: 'all',
|
|
1979
3865
|
test: (ast, componentName, componentSpec) => {
|
|
1980
3866
|
const violations = [];
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
3867
|
+
// Skip if no dependencies
|
|
3868
|
+
if (!componentSpec?.dependencies || componentSpec.dependencies.length === 0) {
|
|
3869
|
+
return violations;
|
|
3870
|
+
}
|
|
3871
|
+
// Build a map of dependency components and their expected props
|
|
3872
|
+
const dependencyPropsMap = new Map();
|
|
3873
|
+
for (const dep of componentSpec.dependencies) {
|
|
3874
|
+
const requiredProps = dep.properties
|
|
3875
|
+
?.filter(p => p.required)
|
|
3876
|
+
?.map(p => p.name) || [];
|
|
3877
|
+
const allProps = dep.properties?.map(p => p.name) || [];
|
|
3878
|
+
dependencyPropsMap.set(dep.name, {
|
|
3879
|
+
required: requiredProps,
|
|
3880
|
+
all: allProps,
|
|
3881
|
+
location: dep.location || 'embedded'
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
// Helper function to find closest matching prop name
|
|
3885
|
+
function findClosestMatch(target, candidates) {
|
|
3886
|
+
if (candidates.length === 0)
|
|
3887
|
+
return null;
|
|
3888
|
+
// Simple Levenshtein distance implementation
|
|
3889
|
+
function levenshtein(a, b) {
|
|
3890
|
+
const matrix = [];
|
|
3891
|
+
for (let i = 0; i <= b.length; i++) {
|
|
3892
|
+
matrix[i] = [i];
|
|
3893
|
+
}
|
|
3894
|
+
for (let j = 0; j <= a.length; j++) {
|
|
3895
|
+
matrix[0][j] = j;
|
|
3896
|
+
}
|
|
3897
|
+
for (let i = 1; i <= b.length; i++) {
|
|
3898
|
+
for (let j = 1; j <= a.length; j++) {
|
|
3899
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
3900
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
3901
|
+
}
|
|
3902
|
+
else {
|
|
3903
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
return matrix[b.length][a.length];
|
|
3908
|
+
}
|
|
3909
|
+
// Find the closest match
|
|
3910
|
+
let bestMatch = '';
|
|
3911
|
+
let bestDistance = Infinity;
|
|
3912
|
+
for (const candidate of candidates) {
|
|
3913
|
+
const distance = levenshtein(target.toLowerCase(), candidate.toLowerCase());
|
|
3914
|
+
if (distance < bestDistance && distance <= 3) { // Max distance of 3 for suggestions
|
|
3915
|
+
bestDistance = distance;
|
|
3916
|
+
bestMatch = candidate;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
return bestMatch || null;
|
|
3920
|
+
}
|
|
3921
|
+
// Standard props that are always valid (passed by the runtime)
|
|
3922
|
+
const standardProps = new Set([
|
|
3923
|
+
'styles', 'utilities', 'components', 'callbacks',
|
|
3924
|
+
'savedUserSettings', 'onSaveUserSettings'
|
|
3925
|
+
]);
|
|
3926
|
+
// Track JSX elements and their props
|
|
1985
3927
|
(0, traverse_1.default)(ast, {
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
3928
|
+
JSXElement(path) {
|
|
3929
|
+
const openingElement = path.node.openingElement;
|
|
3930
|
+
// Get the element name
|
|
3931
|
+
let elementName = '';
|
|
3932
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
3933
|
+
elementName = openingElement.name.name;
|
|
3934
|
+
}
|
|
3935
|
+
else if (t.isJSXMemberExpression(openingElement.name)) {
|
|
3936
|
+
// Handle cases like <MaterialUI.Button>
|
|
3937
|
+
return; // Skip member expressions for now
|
|
3938
|
+
}
|
|
3939
|
+
// Check if this is one of our dependencies
|
|
3940
|
+
if (dependencyPropsMap.has(elementName)) {
|
|
3941
|
+
const { required, all, location } = dependencyPropsMap.get(elementName);
|
|
3942
|
+
// Get passed props
|
|
3943
|
+
const passedProps = new Set();
|
|
3944
|
+
const propLocations = new Map();
|
|
3945
|
+
for (const attr of openingElement.attributes) {
|
|
3946
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
3947
|
+
const propName = attr.name.name;
|
|
3948
|
+
passedProps.add(propName);
|
|
3949
|
+
propLocations.set(propName, {
|
|
3950
|
+
line: attr.loc?.start.line || 0,
|
|
3951
|
+
column: attr.loc?.start.column || 0
|
|
3952
|
+
});
|
|
2000
3953
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
3954
|
+
}
|
|
3955
|
+
// Check for missing required props
|
|
3956
|
+
for (const requiredProp of required) {
|
|
3957
|
+
if (!passedProps.has(requiredProp) && !standardProps.has(requiredProp)) {
|
|
2004
3958
|
violations.push({
|
|
2005
|
-
rule: '
|
|
3959
|
+
rule: 'dependency-prop-validation',
|
|
2006
3960
|
severity: 'critical',
|
|
2007
|
-
line:
|
|
2008
|
-
column:
|
|
2009
|
-
message: `
|
|
3961
|
+
line: openingElement.loc?.start.line || 0,
|
|
3962
|
+
column: openingElement.loc?.start.column || 0,
|
|
3963
|
+
message: `Missing required prop '${requiredProp}' for dependency component '${elementName}'`,
|
|
3964
|
+
code: `<${elementName} ... />`
|
|
2010
3965
|
});
|
|
2011
3966
|
}
|
|
2012
3967
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
3968
|
+
// Check for unknown props (potential typos)
|
|
3969
|
+
for (const passedProp of passedProps) {
|
|
3970
|
+
// Skip standard props and spread operators
|
|
3971
|
+
if (standardProps.has(passedProp) || passedProp === 'key' || passedProp === 'ref') {
|
|
3972
|
+
continue;
|
|
3973
|
+
}
|
|
3974
|
+
if (!all.includes(passedProp)) {
|
|
3975
|
+
// Try to find a close match
|
|
3976
|
+
const suggestion = findClosestMatch(passedProp, all);
|
|
3977
|
+
if (suggestion) {
|
|
3978
|
+
const loc = propLocations.get(passedProp);
|
|
3979
|
+
violations.push({
|
|
3980
|
+
rule: 'dependency-prop-validation',
|
|
3981
|
+
severity: 'high',
|
|
3982
|
+
line: loc?.line || openingElement.loc?.start.line || 0,
|
|
3983
|
+
column: loc?.column || openingElement.loc?.start.column || 0,
|
|
3984
|
+
message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Did you mean '${suggestion}'?`,
|
|
3985
|
+
code: `${passedProp}={...}`
|
|
3986
|
+
});
|
|
2032
3987
|
}
|
|
2033
|
-
|
|
3988
|
+
else {
|
|
3989
|
+
const loc = propLocations.get(passedProp);
|
|
2034
3990
|
violations.push({
|
|
2035
|
-
rule: '
|
|
2036
|
-
severity: '
|
|
2037
|
-
line:
|
|
2038
|
-
column:
|
|
2039
|
-
message: `
|
|
3991
|
+
rule: 'dependency-prop-validation',
|
|
3992
|
+
severity: 'medium',
|
|
3993
|
+
line: loc?.line || openingElement.loc?.start.line || 0,
|
|
3994
|
+
column: loc?.column || openingElement.loc?.start.column || 0,
|
|
3995
|
+
message: `Unknown prop '${passedProp}' passed to dependency component '${elementName}'. Expected props: ${all.join(', ')}`,
|
|
3996
|
+
code: `${passedProp}={...}`
|
|
2040
3997
|
});
|
|
2041
3998
|
}
|
|
2042
3999
|
}
|