@memberjunction/react-test-harness 2.95.0 → 2.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/component-linter.d.ts +2 -0
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +928 -6
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +30 -11
- package/dist/lib/component-runner.js.map +1 -1
- package/dist/lib/styles-type-analyzer.d.ts +64 -0
- package/dist/lib/styles-type-analyzer.d.ts.map +1 -0
- package/dist/lib/styles-type-analyzer.js +265 -0
- package/dist/lib/styles-type-analyzer.js.map +1 -0
- package/package.json +6 -6
|
@@ -30,7 +30,9 @@ exports.ComponentLinter = void 0;
|
|
|
30
30
|
const parser = __importStar(require("@babel/parser"));
|
|
31
31
|
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
32
32
|
const t = __importStar(require("@babel/types"));
|
|
33
|
+
const core_entities_1 = require("@memberjunction/core-entities");
|
|
33
34
|
const library_lint_cache_1 = require("./library-lint-cache");
|
|
35
|
+
const styles_type_analyzer_1 = require("./styles-type-analyzer");
|
|
34
36
|
// Standard HTML elements (lowercase)
|
|
35
37
|
const HTML_ELEMENTS = new Set([
|
|
36
38
|
// Main root
|
|
@@ -99,6 +101,13 @@ const runViewResultProps = [
|
|
|
99
101
|
'TotalRowCount', 'ExecutionTime', 'ErrorMessage'
|
|
100
102
|
];
|
|
101
103
|
class ComponentLinter {
|
|
104
|
+
// Get or create the styles analyzer instance
|
|
105
|
+
static getStylesAnalyzer() {
|
|
106
|
+
if (!ComponentLinter.stylesAnalyzer) {
|
|
107
|
+
ComponentLinter.stylesAnalyzer = new styles_type_analyzer_1.StylesTypeAnalyzer();
|
|
108
|
+
}
|
|
109
|
+
return ComponentLinter.stylesAnalyzer;
|
|
110
|
+
}
|
|
102
111
|
// Helper method to check if a statement contains a return
|
|
103
112
|
static containsReturn(node) {
|
|
104
113
|
let hasReturn = false;
|
|
@@ -1683,6 +1692,46 @@ setData(queryResult.Results || []); // NOT queryResult directly!
|
|
|
1683
1692
|
// }`
|
|
1684
1693
|
});
|
|
1685
1694
|
break;
|
|
1695
|
+
case 'styles-invalid-path':
|
|
1696
|
+
suggestions.push({
|
|
1697
|
+
violation: violation.rule,
|
|
1698
|
+
suggestion: 'Fix invalid styles property paths. Use the correct ComponentStyles interface structure.',
|
|
1699
|
+
example: `// ❌ WRONG - Invalid property paths:
|
|
1700
|
+
styles.fontSize.small // fontSize is not at root level
|
|
1701
|
+
styles.colors.background // colors.background exists
|
|
1702
|
+
styles.spacing.small // should be styles.spacing.sm
|
|
1703
|
+
|
|
1704
|
+
// ✅ CORRECT - Valid property paths:
|
|
1705
|
+
styles.typography.fontSize.sm // fontSize is under typography
|
|
1706
|
+
styles.colors.background // correct path
|
|
1707
|
+
styles.spacing.sm // correct size name
|
|
1708
|
+
|
|
1709
|
+
// With safe access and fallbacks:
|
|
1710
|
+
styles?.typography?.fontSize?.sm || '14px'
|
|
1711
|
+
styles?.colors?.background || '#FFFFFF'
|
|
1712
|
+
styles?.spacing?.sm || '8px'`
|
|
1713
|
+
});
|
|
1714
|
+
break;
|
|
1715
|
+
case 'styles-unsafe-access':
|
|
1716
|
+
suggestions.push({
|
|
1717
|
+
violation: violation.rule,
|
|
1718
|
+
suggestion: 'Use optional chaining for nested styles access to prevent runtime errors.',
|
|
1719
|
+
example: `// ❌ UNSAFE - Direct nested access:
|
|
1720
|
+
const fontSize = styles.typography.fontSize.md;
|
|
1721
|
+
const borderRadius = styles.borders.radius.sm;
|
|
1722
|
+
|
|
1723
|
+
// ✅ SAFE - With optional chaining and fallbacks:
|
|
1724
|
+
const fontSize = styles?.typography?.fontSize?.md || '14px';
|
|
1725
|
+
const borderRadius = styles?.borders?.radius?.sm || '6px';
|
|
1726
|
+
|
|
1727
|
+
// Even better - destructure with defaults:
|
|
1728
|
+
const {
|
|
1729
|
+
typography: {
|
|
1730
|
+
fontSize: { md: fontSize = '14px' } = {}
|
|
1731
|
+
} = {}
|
|
1732
|
+
} = styles || {};`
|
|
1733
|
+
});
|
|
1734
|
+
break;
|
|
1686
1735
|
}
|
|
1687
1736
|
}
|
|
1688
1737
|
return suggestions;
|
|
@@ -4044,6 +4093,117 @@ Valid properties: EntityName, ExtraFilter, Fields, OrderBy, MaxRows, StartRow, R
|
|
|
4044
4093
|
code: `${propName}: ...`
|
|
4045
4094
|
});
|
|
4046
4095
|
}
|
|
4096
|
+
else {
|
|
4097
|
+
// Property name is valid, now check its type
|
|
4098
|
+
const value = prop.value;
|
|
4099
|
+
// Helper to check if a node is null or undefined
|
|
4100
|
+
const isNullOrUndefined = (node) => {
|
|
4101
|
+
return t.isNullLiteral(node) ||
|
|
4102
|
+
(t.isIdentifier(node) && node.name === 'undefined');
|
|
4103
|
+
};
|
|
4104
|
+
// Helper to check if a node could evaluate to a string
|
|
4105
|
+
const isStringLike = (node, depth = 0) => {
|
|
4106
|
+
// Prevent infinite recursion
|
|
4107
|
+
if (depth > 3)
|
|
4108
|
+
return false;
|
|
4109
|
+
// Special handling for ternary operators - check both branches
|
|
4110
|
+
if (t.isConditionalExpression(node)) {
|
|
4111
|
+
const consequentOk = isStringLike(node.consequent, depth + 1) || isNullOrUndefined(node.consequent);
|
|
4112
|
+
const alternateOk = isStringLike(node.alternate, depth + 1) || isNullOrUndefined(node.alternate);
|
|
4113
|
+
return consequentOk && alternateOk;
|
|
4114
|
+
}
|
|
4115
|
+
// Explicitly reject object and array expressions
|
|
4116
|
+
if (t.isObjectExpression(node) || t.isArrayExpression(node)) {
|
|
4117
|
+
return false;
|
|
4118
|
+
}
|
|
4119
|
+
return t.isStringLiteral(node) ||
|
|
4120
|
+
t.isTemplateLiteral(node) ||
|
|
4121
|
+
t.isBinaryExpression(node) || // String concatenation
|
|
4122
|
+
t.isIdentifier(node) || // Variable
|
|
4123
|
+
t.isCallExpression(node) || // Function call
|
|
4124
|
+
t.isMemberExpression(node); // Property access
|
|
4125
|
+
};
|
|
4126
|
+
// Helper to check if a node could evaluate to a number
|
|
4127
|
+
const isNumberLike = (node) => {
|
|
4128
|
+
return t.isNumericLiteral(node) ||
|
|
4129
|
+
t.isBinaryExpression(node) || // Math operations
|
|
4130
|
+
t.isUnaryExpression(node) || // Negative numbers, etc
|
|
4131
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4132
|
+
t.isIdentifier(node) || // Variable
|
|
4133
|
+
t.isCallExpression(node) || // Function call
|
|
4134
|
+
t.isMemberExpression(node); // Property access
|
|
4135
|
+
};
|
|
4136
|
+
// Helper to check if a node is array-like
|
|
4137
|
+
const isArrayLike = (node) => {
|
|
4138
|
+
return t.isArrayExpression(node) ||
|
|
4139
|
+
t.isIdentifier(node) || // Variable
|
|
4140
|
+
t.isCallExpression(node) || // Function returning array
|
|
4141
|
+
t.isMemberExpression(node) || // Property access
|
|
4142
|
+
t.isConditionalExpression(node); // Ternary
|
|
4143
|
+
};
|
|
4144
|
+
// Helper to check if a node is object-like (but not array)
|
|
4145
|
+
const isObjectLike = (node) => {
|
|
4146
|
+
if (t.isArrayExpression(node))
|
|
4147
|
+
return false;
|
|
4148
|
+
return t.isObjectExpression(node) ||
|
|
4149
|
+
t.isIdentifier(node) || // Variable
|
|
4150
|
+
t.isCallExpression(node) || // Function returning object
|
|
4151
|
+
t.isMemberExpression(node) || // Property access
|
|
4152
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4153
|
+
t.isSpreadElement(node); // Spread syntax (though this is the problem case)
|
|
4154
|
+
};
|
|
4155
|
+
// Validate types based on property name
|
|
4156
|
+
if (propName === 'ExtraFilter' || propName === 'OrderBy' || propName === 'EntityName') {
|
|
4157
|
+
// These must be strings (ExtraFilter and OrderBy can also be null/undefined)
|
|
4158
|
+
const allowNullUndefined = propName === 'ExtraFilter' || propName === 'OrderBy';
|
|
4159
|
+
if (!isStringLike(value) && !(allowNullUndefined && isNullOrUndefined(value))) {
|
|
4160
|
+
let exampleValue = '';
|
|
4161
|
+
if (propName === 'ExtraFilter') {
|
|
4162
|
+
exampleValue = `"Status = 'Active' AND Type = 'Customer'"`;
|
|
4163
|
+
}
|
|
4164
|
+
else if (propName === 'OrderBy') {
|
|
4165
|
+
exampleValue = `"CreatedAt DESC"`;
|
|
4166
|
+
}
|
|
4167
|
+
else if (propName === 'EntityName') {
|
|
4168
|
+
exampleValue = `"Products"`;
|
|
4169
|
+
}
|
|
4170
|
+
violations.push({
|
|
4171
|
+
rule: 'runview-runquery-valid-properties',
|
|
4172
|
+
severity: 'critical',
|
|
4173
|
+
line: prop.loc?.start.line || 0,
|
|
4174
|
+
column: prop.loc?.start.column || 0,
|
|
4175
|
+
message: `${methodName} property '${propName}' must be a string, not ${t.isObjectExpression(value) ? 'an object' : t.isArrayExpression(value) ? 'an array' : 'a non-string value'}. Example: ${propName}: ${exampleValue}`,
|
|
4176
|
+
code: `${propName}: ${prop.value.type === 'ObjectExpression' ? '{...}' : prop.value.type === 'ArrayExpression' ? '[...]' : '...'}`
|
|
4177
|
+
});
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
else if (propName === 'Fields') {
|
|
4181
|
+
// Fields must be an array of strings (or a string that we'll interpret as comma-separated)
|
|
4182
|
+
if (!isArrayLike(value) && !isStringLike(value)) {
|
|
4183
|
+
violations.push({
|
|
4184
|
+
rule: 'runview-runquery-valid-properties',
|
|
4185
|
+
severity: 'critical',
|
|
4186
|
+
line: prop.loc?.start.line || 0,
|
|
4187
|
+
column: prop.loc?.start.column || 0,
|
|
4188
|
+
message: `${methodName} property 'Fields' must be an array of field names or a comma-separated string. Example: Fields: ['ID', 'Name', 'Status'] or Fields: 'ID, Name, Status'`,
|
|
4189
|
+
code: `Fields: ${prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
else if (propName === 'MaxRows' || propName === 'StartRow') {
|
|
4194
|
+
// These must be numbers
|
|
4195
|
+
if (!isNumberLike(value)) {
|
|
4196
|
+
violations.push({
|
|
4197
|
+
rule: 'runview-runquery-valid-properties',
|
|
4198
|
+
severity: 'critical',
|
|
4199
|
+
line: prop.loc?.start.line || 0,
|
|
4200
|
+
column: prop.loc?.start.column || 0,
|
|
4201
|
+
message: `${methodName} property '${propName}' must be a number. Example: ${propName}: ${propName === 'MaxRows' ? '100' : '0'}`,
|
|
4202
|
+
code: `${propName}: ${prop.value.type === 'StringLiteral' ? '"..."' : prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4047
4207
|
}
|
|
4048
4208
|
}
|
|
4049
4209
|
// Check that EntityName is present (required property)
|
|
@@ -4148,6 +4308,111 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4148
4308
|
code: `${propName}: ...`
|
|
4149
4309
|
});
|
|
4150
4310
|
}
|
|
4311
|
+
else {
|
|
4312
|
+
// Property name is valid, now check its type
|
|
4313
|
+
const value = prop.value;
|
|
4314
|
+
// Helper to check if a node is null or undefined
|
|
4315
|
+
const isNullOrUndefined = (node) => {
|
|
4316
|
+
return t.isNullLiteral(node) ||
|
|
4317
|
+
(t.isIdentifier(node) && node.name === 'undefined');
|
|
4318
|
+
};
|
|
4319
|
+
// Helper to check if a node could evaluate to a string
|
|
4320
|
+
const isStringLike = (node, depth = 0) => {
|
|
4321
|
+
// Prevent infinite recursion
|
|
4322
|
+
if (depth > 3)
|
|
4323
|
+
return false;
|
|
4324
|
+
// Special handling for ternary operators - check both branches
|
|
4325
|
+
if (t.isConditionalExpression(node)) {
|
|
4326
|
+
const consequentOk = isStringLike(node.consequent, depth + 1) || isNullOrUndefined(node.consequent);
|
|
4327
|
+
const alternateOk = isStringLike(node.alternate, depth + 1) || isNullOrUndefined(node.alternate);
|
|
4328
|
+
return consequentOk && alternateOk;
|
|
4329
|
+
}
|
|
4330
|
+
// Explicitly reject object and array expressions
|
|
4331
|
+
if (t.isObjectExpression(node) || t.isArrayExpression(node)) {
|
|
4332
|
+
return false;
|
|
4333
|
+
}
|
|
4334
|
+
return t.isStringLiteral(node) ||
|
|
4335
|
+
t.isTemplateLiteral(node) ||
|
|
4336
|
+
t.isBinaryExpression(node) || // String concatenation
|
|
4337
|
+
t.isIdentifier(node) || // Variable
|
|
4338
|
+
t.isCallExpression(node) || // Function call
|
|
4339
|
+
t.isMemberExpression(node); // Property access
|
|
4340
|
+
};
|
|
4341
|
+
// Helper to check if a node could evaluate to a number
|
|
4342
|
+
const isNumberLike = (node) => {
|
|
4343
|
+
return t.isNumericLiteral(node) ||
|
|
4344
|
+
t.isBinaryExpression(node) || // Math operations
|
|
4345
|
+
t.isUnaryExpression(node) || // Negative numbers, etc
|
|
4346
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4347
|
+
t.isIdentifier(node) || // Variable
|
|
4348
|
+
t.isCallExpression(node) || // Function call
|
|
4349
|
+
t.isMemberExpression(node); // Property access
|
|
4350
|
+
};
|
|
4351
|
+
// Helper to check if a node is object-like (but not array)
|
|
4352
|
+
const isObjectLike = (node) => {
|
|
4353
|
+
if (t.isArrayExpression(node))
|
|
4354
|
+
return false;
|
|
4355
|
+
return t.isObjectExpression(node) ||
|
|
4356
|
+
t.isIdentifier(node) || // Variable
|
|
4357
|
+
t.isCallExpression(node) || // Function returning object
|
|
4358
|
+
t.isMemberExpression(node) || // Property access
|
|
4359
|
+
t.isConditionalExpression(node) || // Ternary
|
|
4360
|
+
t.isSpreadElement(node); // Spread syntax
|
|
4361
|
+
};
|
|
4362
|
+
// Validate types based on property name
|
|
4363
|
+
if (propName === 'QueryID' || propName === 'QueryName' || propName === 'CategoryID' || propName === 'CategoryPath') {
|
|
4364
|
+
// These must be strings
|
|
4365
|
+
if (!isStringLike(value)) {
|
|
4366
|
+
let exampleValue = '';
|
|
4367
|
+
if (propName === 'QueryID') {
|
|
4368
|
+
exampleValue = `"550e8400-e29b-41d4-a716-446655440000"`;
|
|
4369
|
+
}
|
|
4370
|
+
else if (propName === 'QueryName') {
|
|
4371
|
+
exampleValue = `"Sales by Region"`;
|
|
4372
|
+
}
|
|
4373
|
+
else if (propName === 'CategoryID') {
|
|
4374
|
+
exampleValue = `"123e4567-e89b-12d3-a456-426614174000"`;
|
|
4375
|
+
}
|
|
4376
|
+
else if (propName === 'CategoryPath') {
|
|
4377
|
+
exampleValue = `"/Reports/Sales/"`;
|
|
4378
|
+
}
|
|
4379
|
+
violations.push({
|
|
4380
|
+
rule: 'runview-runquery-valid-properties',
|
|
4381
|
+
severity: 'critical',
|
|
4382
|
+
line: prop.loc?.start.line || 0,
|
|
4383
|
+
column: prop.loc?.start.column || 0,
|
|
4384
|
+
message: `RunQuery property '${propName}' must be a string. Example: ${propName}: ${exampleValue}`,
|
|
4385
|
+
code: `${propName}: ${prop.value.type === 'ObjectExpression' ? '{...}' : prop.value.type === 'ArrayExpression' ? '[...]' : '...'}`
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
else if (propName === 'Parameters') {
|
|
4390
|
+
// Parameters must be an object (Record<string, any>)
|
|
4391
|
+
if (!isObjectLike(value)) {
|
|
4392
|
+
violations.push({
|
|
4393
|
+
rule: 'runview-runquery-valid-properties',
|
|
4394
|
+
severity: 'critical',
|
|
4395
|
+
line: prop.loc?.start.line || 0,
|
|
4396
|
+
column: prop.loc?.start.column || 0,
|
|
4397
|
+
message: `RunQuery property 'Parameters' must be an object containing key-value pairs. Example: Parameters: { startDate: '2024-01-01', status: 'Active' }`,
|
|
4398
|
+
code: `Parameters: ${t.isArrayExpression(value) ? '[...]' : t.isStringLiteral(value) ? '"..."' : '...'}`
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
else if (propName === 'MaxRows' || propName === 'StartRow') {
|
|
4403
|
+
// These must be numbers
|
|
4404
|
+
if (!isNumberLike(value)) {
|
|
4405
|
+
violations.push({
|
|
4406
|
+
rule: 'runview-runquery-valid-properties',
|
|
4407
|
+
severity: 'critical',
|
|
4408
|
+
line: prop.loc?.start.line || 0,
|
|
4409
|
+
column: prop.loc?.start.column || 0,
|
|
4410
|
+
message: `RunQuery property '${propName}' must be a number. Example: ${propName}: ${propName === 'MaxRows' ? '100' : '0'}`,
|
|
4411
|
+
code: `${propName}: ${prop.value.type === 'StringLiteral' ? '"..."' : prop.value.type === 'ObjectExpression' ? '{...}' : '...'}`
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4151
4416
|
}
|
|
4152
4417
|
}
|
|
4153
4418
|
// Check that at least one required property is present
|
|
@@ -4684,8 +4949,10 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4684
4949
|
test: (ast, componentName, componentSpec) => {
|
|
4685
4950
|
const violations = [];
|
|
4686
4951
|
const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
|
|
4687
|
-
//
|
|
4688
|
-
const
|
|
4952
|
+
// React special props that are automatically provided by React
|
|
4953
|
+
const reactSpecialProps = new Set(['children']);
|
|
4954
|
+
// Build set of allowed props: standard props + React special props + componentSpec properties
|
|
4955
|
+
const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
|
|
4689
4956
|
// Add props from componentSpec.properties if they exist
|
|
4690
4957
|
if (componentSpec?.properties) {
|
|
4691
4958
|
for (const prop of componentSpec.properties) {
|
|
@@ -4720,7 +4987,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4720
4987
|
severity: 'critical',
|
|
4721
4988
|
line: path.node.loc?.start.line || 0,
|
|
4722
4989
|
column: path.node.loc?.start.column || 0,
|
|
4723
|
-
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
4990
|
+
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}, React special props: ${Array.from(reactSpecialProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
4724
4991
|
});
|
|
4725
4992
|
}
|
|
4726
4993
|
}
|
|
@@ -4753,7 +5020,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4753
5020
|
severity: 'critical',
|
|
4754
5021
|
line: path.node.loc?.start.line || 0,
|
|
4755
5022
|
column: path.node.loc?.start.column || 0,
|
|
4756
|
-
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
5023
|
+
message: `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. Components can only accept standard props: ${Array.from(standardProps).join(', ')}, React special props: ${Array.from(reactSpecialProps).join(', ')}${customPropsMessage}. All custom props must be defined in the component spec properties array.`
|
|
4757
5024
|
});
|
|
4758
5025
|
}
|
|
4759
5026
|
}
|
|
@@ -4764,6 +5031,228 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4764
5031
|
return violations;
|
|
4765
5032
|
}
|
|
4766
5033
|
},
|
|
5034
|
+
{
|
|
5035
|
+
name: 'validate-dependency-props',
|
|
5036
|
+
appliesTo: 'all',
|
|
5037
|
+
test: (ast, componentName, componentSpec) => {
|
|
5038
|
+
const violations = [];
|
|
5039
|
+
// Build a map of dependency components to their specs
|
|
5040
|
+
const dependencySpecs = new Map();
|
|
5041
|
+
// Process embedded dependencies
|
|
5042
|
+
if (componentSpec?.dependencies && Array.isArray(componentSpec.dependencies)) {
|
|
5043
|
+
for (const dep of componentSpec.dependencies) {
|
|
5044
|
+
if (dep && dep.name) {
|
|
5045
|
+
if (dep.location === 'registry') {
|
|
5046
|
+
const match = core_entities_1.ComponentMetadataEngine.Instance.FindComponent(dep.name, dep.namespace, dep.registry);
|
|
5047
|
+
if (!match) {
|
|
5048
|
+
// the specified registry component was not found, we can't lint for it, but we should put a warning
|
|
5049
|
+
console.warn('Dependency component not found in registry', dep);
|
|
5050
|
+
}
|
|
5051
|
+
else {
|
|
5052
|
+
dependencySpecs.set(dep.name, match.spec);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
else {
|
|
5056
|
+
// Embedded dependencies have their spec inline
|
|
5057
|
+
dependencySpecs.set(dep.name, dep);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
else {
|
|
5061
|
+
// we have an invalid dep in the spec, not a fatal error but we should log this
|
|
5062
|
+
console.warn(`Invalid dependency in component spec`, dep);
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
// For registry dependencies, we'd need ComponentMetadataEngine
|
|
5067
|
+
// But since this is a static lint check, we'll focus on embedded deps
|
|
5068
|
+
// Registry components would need async loading which doesn't fit the current sync pattern
|
|
5069
|
+
// Now traverse JSX to find component usage
|
|
5070
|
+
(0, traverse_1.default)(ast, {
|
|
5071
|
+
JSXElement(path) {
|
|
5072
|
+
const openingElement = path.node.openingElement;
|
|
5073
|
+
// Check if this is one of our dependency components
|
|
5074
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
5075
|
+
const componentName = openingElement.name.name;
|
|
5076
|
+
const depSpec = dependencySpecs.get(componentName);
|
|
5077
|
+
if (depSpec) {
|
|
5078
|
+
// Collect props being passed
|
|
5079
|
+
const passedProps = new Set();
|
|
5080
|
+
const passedPropNodes = new Map();
|
|
5081
|
+
for (const attr of openingElement.attributes) {
|
|
5082
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
5083
|
+
const propName = attr.name.name;
|
|
5084
|
+
passedProps.add(propName);
|
|
5085
|
+
passedPropNodes.set(propName, attr);
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
// Check required custom props
|
|
5089
|
+
if (depSpec.properties && Array.isArray(depSpec.properties)) {
|
|
5090
|
+
const requiredProps = [];
|
|
5091
|
+
const optionalProps = [];
|
|
5092
|
+
for (const prop of depSpec.properties) {
|
|
5093
|
+
if (prop && prop.name && typeof prop.name === 'string') {
|
|
5094
|
+
if (prop.required === true) {
|
|
5095
|
+
requiredProps.push(prop.name);
|
|
5096
|
+
}
|
|
5097
|
+
else {
|
|
5098
|
+
optionalProps.push(prop.name);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
// Check for missing required props
|
|
5103
|
+
const missingRequired = requiredProps.filter(prop => {
|
|
5104
|
+
// Special handling for 'children' prop
|
|
5105
|
+
if (prop === 'children') {
|
|
5106
|
+
// Check if JSX element has children nodes
|
|
5107
|
+
const hasChildren = path.node.children && path.node.children.length > 0 &&
|
|
5108
|
+
path.node.children.some(child => !t.isJSXText(child) || (t.isJSXText(child) && child.value.trim() !== ''));
|
|
5109
|
+
return !passedProps.has(prop) && !hasChildren;
|
|
5110
|
+
}
|
|
5111
|
+
return !passedProps.has(prop);
|
|
5112
|
+
});
|
|
5113
|
+
// Separate children warnings from other critical props
|
|
5114
|
+
const missingChildren = missingRequired.filter(prop => prop === 'children');
|
|
5115
|
+
const missingOtherProps = missingRequired.filter(prop => prop !== 'children');
|
|
5116
|
+
// Critical violation for non-children required props
|
|
5117
|
+
if (missingOtherProps.length > 0) {
|
|
5118
|
+
violations.push({
|
|
5119
|
+
rule: 'validate-dependency-props',
|
|
5120
|
+
severity: 'critical',
|
|
5121
|
+
line: openingElement.loc?.start.line || 0,
|
|
5122
|
+
column: openingElement.loc?.start.column || 0,
|
|
5123
|
+
message: `Dependency component "${componentName}" is missing required props: ${missingOtherProps.join(', ')}. These props are marked as required in the component's specification.`,
|
|
5124
|
+
code: `<${componentName} ... />`
|
|
5125
|
+
});
|
|
5126
|
+
}
|
|
5127
|
+
// Medium severity warning for missing children when required
|
|
5128
|
+
if (missingChildren.length > 0) {
|
|
5129
|
+
violations.push({
|
|
5130
|
+
rule: 'validate-dependency-props',
|
|
5131
|
+
severity: 'medium',
|
|
5132
|
+
line: openingElement.loc?.start.line || 0,
|
|
5133
|
+
column: openingElement.loc?.start.column || 0,
|
|
5134
|
+
message: `Component "${componentName}" expects children but none were provided. The 'children' prop is marked as required in the component's specification.`,
|
|
5135
|
+
code: `<${componentName} ... />`
|
|
5136
|
+
});
|
|
5137
|
+
}
|
|
5138
|
+
// Validate prop types for passed props
|
|
5139
|
+
for (const [propName, attrNode] of passedPropNodes) {
|
|
5140
|
+
const propSpec = depSpec.properties.find(p => p.name === propName);
|
|
5141
|
+
if (propSpec && propSpec.type) {
|
|
5142
|
+
const value = attrNode.value;
|
|
5143
|
+
// Type validation based on prop spec type
|
|
5144
|
+
if (propSpec.type === 'string') {
|
|
5145
|
+
// Check if value could be a string
|
|
5146
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5147
|
+
const expr = value.expression;
|
|
5148
|
+
// Check for obvious non-string types
|
|
5149
|
+
if (t.isNumericLiteral(expr) || t.isBooleanLiteral(expr) ||
|
|
5150
|
+
t.isArrayExpression(expr) || (t.isObjectExpression(expr) && !t.isTemplateLiteral(expr))) {
|
|
5151
|
+
violations.push({
|
|
5152
|
+
rule: 'validate-dependency-props',
|
|
5153
|
+
severity: 'high',
|
|
5154
|
+
line: attrNode.loc?.start.line || 0,
|
|
5155
|
+
column: attrNode.loc?.start.column || 0,
|
|
5156
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "string" but received a different type.`,
|
|
5157
|
+
code: `${propName}={...}`
|
|
5158
|
+
});
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
else if (propSpec.type === 'number') {
|
|
5163
|
+
// Check if value could be a number
|
|
5164
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5165
|
+
const expr = value.expression;
|
|
5166
|
+
if (t.isStringLiteral(expr) || t.isBooleanLiteral(expr) ||
|
|
5167
|
+
t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
|
|
5168
|
+
violations.push({
|
|
5169
|
+
rule: 'validate-dependency-props',
|
|
5170
|
+
severity: 'high',
|
|
5171
|
+
line: attrNode.loc?.start.line || 0,
|
|
5172
|
+
column: attrNode.loc?.start.column || 0,
|
|
5173
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "number" but received a different type.`,
|
|
5174
|
+
code: `${propName}={...}`
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
else if (propSpec.type === 'boolean') {
|
|
5180
|
+
// Check if value could be a boolean
|
|
5181
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5182
|
+
const expr = value.expression;
|
|
5183
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
|
|
5184
|
+
t.isArrayExpression(expr) || t.isObjectExpression(expr)) {
|
|
5185
|
+
violations.push({
|
|
5186
|
+
rule: 'validate-dependency-props',
|
|
5187
|
+
severity: 'high',
|
|
5188
|
+
line: attrNode.loc?.start.line || 0,
|
|
5189
|
+
column: attrNode.loc?.start.column || 0,
|
|
5190
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "boolean" but received a different type.`,
|
|
5191
|
+
code: `${propName}={...}`
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
else if (propSpec.type === 'array') {
|
|
5197
|
+
// Check if value could be an array
|
|
5198
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5199
|
+
const expr = value.expression;
|
|
5200
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
|
|
5201
|
+
t.isBooleanLiteral(expr) || (t.isObjectExpression(expr) && !t.isArrayExpression(expr))) {
|
|
5202
|
+
violations.push({
|
|
5203
|
+
rule: 'validate-dependency-props',
|
|
5204
|
+
severity: 'high',
|
|
5205
|
+
line: attrNode.loc?.start.line || 0,
|
|
5206
|
+
column: attrNode.loc?.start.column || 0,
|
|
5207
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "array" but received a different type.`,
|
|
5208
|
+
code: `${propName}={...}`
|
|
5209
|
+
});
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
else if (propSpec.type === 'object') {
|
|
5214
|
+
// Check if value could be an object
|
|
5215
|
+
if (value && t.isJSXExpressionContainer(value)) {
|
|
5216
|
+
const expr = value.expression;
|
|
5217
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr) ||
|
|
5218
|
+
t.isBooleanLiteral(expr) || t.isArrayExpression(expr)) {
|
|
5219
|
+
violations.push({
|
|
5220
|
+
rule: 'validate-dependency-props',
|
|
5221
|
+
severity: 'high',
|
|
5222
|
+
line: attrNode.loc?.start.line || 0,
|
|
5223
|
+
column: attrNode.loc?.start.column || 0,
|
|
5224
|
+
message: `Prop "${propName}" on component "${componentName}" expects type "object" but received a different type.`,
|
|
5225
|
+
code: `${propName}={...}`
|
|
5226
|
+
});
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
// Check for unknown props (props not in the spec)
|
|
5233
|
+
const specPropNames = new Set(depSpec.properties.map(p => p.name).filter(Boolean));
|
|
5234
|
+
const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
|
|
5235
|
+
const reactSpecialProps = new Set(['children']);
|
|
5236
|
+
for (const passedProp of passedProps) {
|
|
5237
|
+
if (!specPropNames.has(passedProp) && !standardProps.has(passedProp) && !reactSpecialProps.has(passedProp)) {
|
|
5238
|
+
violations.push({
|
|
5239
|
+
rule: 'validate-dependency-props',
|
|
5240
|
+
severity: 'medium',
|
|
5241
|
+
line: passedPropNodes.get(passedProp)?.loc?.start.line || 0,
|
|
5242
|
+
column: passedPropNodes.get(passedProp)?.loc?.start.column || 0,
|
|
5243
|
+
message: `Prop "${passedProp}" is not defined in the specification for component "${componentName}". In addition to the standard MJ props, valid custom props: ${Array.from(specPropNames).join(', ') || 'none'}.`,
|
|
5244
|
+
code: `${passedProp}={...}`
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
});
|
|
5253
|
+
return violations;
|
|
5254
|
+
}
|
|
5255
|
+
},
|
|
4767
5256
|
{
|
|
4768
5257
|
name: 'invalid-components-destructuring',
|
|
4769
5258
|
appliesTo: 'all',
|
|
@@ -6903,10 +7392,10 @@ Correct pattern:
|
|
|
6903
7392
|
if (!isUsed) {
|
|
6904
7393
|
violations.push({
|
|
6905
7394
|
rule: 'unused-component-dependencies',
|
|
6906
|
-
severity: '
|
|
7395
|
+
severity: 'low',
|
|
6907
7396
|
line: 1,
|
|
6908
7397
|
column: 0,
|
|
6909
|
-
message: `Component dependency "${depName}" is declared but never used.
|
|
7398
|
+
message: `Component dependency "${depName}" is declared but never used. Consider removing it if not needed.`,
|
|
6910
7399
|
code: `Expected usage: <${depName} /> or <components.${depName} />`
|
|
6911
7400
|
});
|
|
6912
7401
|
}
|
|
@@ -7139,6 +7628,439 @@ Correct pattern:
|
|
|
7139
7628
|
}
|
|
7140
7629
|
return violations;
|
|
7141
7630
|
}
|
|
7631
|
+
},
|
|
7632
|
+
// New rules for catching RunQuery/RunView result access patterns
|
|
7633
|
+
{
|
|
7634
|
+
name: 'runquery-runview-ternary-array-check',
|
|
7635
|
+
appliesTo: 'all',
|
|
7636
|
+
test: (ast, componentName, componentSpec) => {
|
|
7637
|
+
const violations = [];
|
|
7638
|
+
// Track variables that hold RunView/RunQuery results
|
|
7639
|
+
const resultVariables = new Map();
|
|
7640
|
+
// First pass: identify all RunView/RunQuery calls and their assigned variables
|
|
7641
|
+
(0, traverse_1.default)(ast, {
|
|
7642
|
+
AwaitExpression(path) {
|
|
7643
|
+
const callExpr = path.node.argument;
|
|
7644
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
7645
|
+
const callee = callExpr.callee;
|
|
7646
|
+
// Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
|
|
7647
|
+
if (t.isMemberExpression(callee.object) &&
|
|
7648
|
+
t.isIdentifier(callee.object.object) &&
|
|
7649
|
+
callee.object.object.name === 'utilities' &&
|
|
7650
|
+
t.isIdentifier(callee.object.property)) {
|
|
7651
|
+
const subObject = callee.object.property.name;
|
|
7652
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
7653
|
+
let methodType = null;
|
|
7654
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
7655
|
+
methodType = method;
|
|
7656
|
+
}
|
|
7657
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
7658
|
+
methodType = 'RunQuery';
|
|
7659
|
+
}
|
|
7660
|
+
if (methodType) {
|
|
7661
|
+
// Check if this is being assigned to a variable
|
|
7662
|
+
const parent = path.parent;
|
|
7663
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
7664
|
+
// const result = await utilities.rv.RunView(...)
|
|
7665
|
+
resultVariables.set(parent.id.name, {
|
|
7666
|
+
line: parent.id.loc?.start.line || 0,
|
|
7667
|
+
column: parent.id.loc?.start.column || 0,
|
|
7668
|
+
method: methodType,
|
|
7669
|
+
varName: parent.id.name
|
|
7670
|
+
});
|
|
7671
|
+
}
|
|
7672
|
+
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
7673
|
+
// result = await utilities.rv.RunView(...)
|
|
7674
|
+
resultVariables.set(parent.left.name, {
|
|
7675
|
+
line: parent.left.loc?.start.line || 0,
|
|
7676
|
+
column: parent.left.loc?.start.column || 0,
|
|
7677
|
+
method: methodType,
|
|
7678
|
+
varName: parent.left.name
|
|
7679
|
+
});
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
});
|
|
7686
|
+
// Second pass: check for Array.isArray(result) ? result : [] pattern
|
|
7687
|
+
(0, traverse_1.default)(ast, {
|
|
7688
|
+
ConditionalExpression(path) {
|
|
7689
|
+
const test = path.node.test;
|
|
7690
|
+
const consequent = path.node.consequent;
|
|
7691
|
+
const alternate = path.node.alternate;
|
|
7692
|
+
// Check for Array.isArray(variable) pattern
|
|
7693
|
+
if (t.isCallExpression(test) &&
|
|
7694
|
+
t.isMemberExpression(test.callee) &&
|
|
7695
|
+
t.isIdentifier(test.callee.object) &&
|
|
7696
|
+
test.callee.object.name === 'Array' &&
|
|
7697
|
+
t.isIdentifier(test.callee.property) &&
|
|
7698
|
+
test.callee.property.name === 'isArray' &&
|
|
7699
|
+
test.arguments.length === 1 &&
|
|
7700
|
+
t.isIdentifier(test.arguments[0])) {
|
|
7701
|
+
const varName = test.arguments[0].name;
|
|
7702
|
+
// Check if this variable is a RunQuery/RunView result
|
|
7703
|
+
if (resultVariables.has(varName)) {
|
|
7704
|
+
const resultInfo = resultVariables.get(varName);
|
|
7705
|
+
// Check if the consequent is the same variable and alternate is []
|
|
7706
|
+
if (t.isIdentifier(consequent) &&
|
|
7707
|
+
consequent.name === varName &&
|
|
7708
|
+
t.isArrayExpression(alternate) &&
|
|
7709
|
+
alternate.elements.length === 0) {
|
|
7710
|
+
violations.push({
|
|
7711
|
+
rule: 'runquery-runview-ternary-array-check',
|
|
7712
|
+
severity: 'critical',
|
|
7713
|
+
line: test.loc?.start.line || 0,
|
|
7714
|
+
column: test.loc?.start.column || 0,
|
|
7715
|
+
message: `${resultInfo.method} never returns an array directly. The pattern "Array.isArray(${varName}) ? ${varName} : []" will always evaluate to [] because ${varName} is an object with { Success, Results, ErrorMessage }.
|
|
7716
|
+
|
|
7717
|
+
Correct patterns:
|
|
7718
|
+
// Option 1: Simple with fallback
|
|
7719
|
+
${varName}.Results || []
|
|
7720
|
+
|
|
7721
|
+
// Option 2: Check success first
|
|
7722
|
+
if (${varName}.Success) {
|
|
7723
|
+
setData(${varName}.Results || []);
|
|
7724
|
+
} else {
|
|
7725
|
+
console.error('Failed:', ${varName}.ErrorMessage);
|
|
7726
|
+
setData([]);
|
|
7727
|
+
}`,
|
|
7728
|
+
code: `Array.isArray(${varName}) ? ${varName} : []`
|
|
7729
|
+
});
|
|
7730
|
+
}
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
});
|
|
7735
|
+
return violations;
|
|
7736
|
+
}
|
|
7737
|
+
},
|
|
7738
|
+
{
|
|
7739
|
+
name: 'runquery-runview-direct-setstate',
|
|
7740
|
+
appliesTo: 'all',
|
|
7741
|
+
test: (ast, componentName, componentSpec) => {
|
|
7742
|
+
const violations = [];
|
|
7743
|
+
// Track variables that hold RunView/RunQuery results
|
|
7744
|
+
const resultVariables = new Map();
|
|
7745
|
+
// First pass: identify all RunView/RunQuery calls and their assigned variables
|
|
7746
|
+
(0, traverse_1.default)(ast, {
|
|
7747
|
+
AwaitExpression(path) {
|
|
7748
|
+
const callExpr = path.node.argument;
|
|
7749
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
7750
|
+
const callee = callExpr.callee;
|
|
7751
|
+
// Check for utilities.rv.RunView/RunViews or utilities.rq.RunQuery pattern
|
|
7752
|
+
if (t.isMemberExpression(callee.object) &&
|
|
7753
|
+
t.isIdentifier(callee.object.object) &&
|
|
7754
|
+
callee.object.object.name === 'utilities' &&
|
|
7755
|
+
t.isIdentifier(callee.object.property)) {
|
|
7756
|
+
const subObject = callee.object.property.name;
|
|
7757
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
7758
|
+
let methodType = null;
|
|
7759
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
7760
|
+
methodType = method;
|
|
7761
|
+
}
|
|
7762
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
7763
|
+
methodType = 'RunQuery';
|
|
7764
|
+
}
|
|
7765
|
+
if (methodType) {
|
|
7766
|
+
// Check if this is being assigned to a variable
|
|
7767
|
+
const parent = path.parent;
|
|
7768
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
7769
|
+
resultVariables.set(parent.id.name, {
|
|
7770
|
+
line: parent.id.loc?.start.line || 0,
|
|
7771
|
+
column: parent.id.loc?.start.column || 0,
|
|
7772
|
+
method: methodType,
|
|
7773
|
+
varName: parent.id.name
|
|
7774
|
+
});
|
|
7775
|
+
}
|
|
7776
|
+
else if (t.isAssignmentExpression(parent) && t.isIdentifier(parent.left)) {
|
|
7777
|
+
resultVariables.set(parent.left.name, {
|
|
7778
|
+
line: parent.left.loc?.start.line || 0,
|
|
7779
|
+
column: parent.left.loc?.start.column || 0,
|
|
7780
|
+
method: methodType,
|
|
7781
|
+
varName: parent.left.name
|
|
7782
|
+
});
|
|
7783
|
+
}
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
});
|
|
7789
|
+
// Second pass: check for passing result directly to setState functions
|
|
7790
|
+
(0, traverse_1.default)(ast, {
|
|
7791
|
+
CallExpression(path) {
|
|
7792
|
+
const callee = path.node.callee;
|
|
7793
|
+
// Check if this is a setState function call
|
|
7794
|
+
if (t.isIdentifier(callee)) {
|
|
7795
|
+
const funcName = callee.name;
|
|
7796
|
+
// Common setState patterns
|
|
7797
|
+
const setStatePatterns = [
|
|
7798
|
+
/^set[A-Z]/, // setData, setChartData, setItems, etc.
|
|
7799
|
+
/^update[A-Z]/, // updateData, updateItems, etc.
|
|
7800
|
+
];
|
|
7801
|
+
const isSetStateFunction = setStatePatterns.some(pattern => pattern.test(funcName));
|
|
7802
|
+
if (isSetStateFunction && path.node.arguments.length > 0) {
|
|
7803
|
+
const firstArg = path.node.arguments[0];
|
|
7804
|
+
// Check if the argument is a ternary with Array.isArray check
|
|
7805
|
+
if (t.isConditionalExpression(firstArg)) {
|
|
7806
|
+
const test = firstArg.test;
|
|
7807
|
+
const consequent = firstArg.consequent;
|
|
7808
|
+
const alternate = firstArg.alternate;
|
|
7809
|
+
// Check for Array.isArray(variable) ? variable : []
|
|
7810
|
+
if (t.isCallExpression(test) &&
|
|
7811
|
+
t.isMemberExpression(test.callee) &&
|
|
7812
|
+
t.isIdentifier(test.callee.object) &&
|
|
7813
|
+
test.callee.object.name === 'Array' &&
|
|
7814
|
+
t.isIdentifier(test.callee.property) &&
|
|
7815
|
+
test.callee.property.name === 'isArray' &&
|
|
7816
|
+
test.arguments.length === 1 &&
|
|
7817
|
+
t.isIdentifier(test.arguments[0])) {
|
|
7818
|
+
const varName = test.arguments[0].name;
|
|
7819
|
+
if (resultVariables.has(varName) &&
|
|
7820
|
+
t.isIdentifier(consequent) &&
|
|
7821
|
+
consequent.name === varName) {
|
|
7822
|
+
const resultInfo = resultVariables.get(varName);
|
|
7823
|
+
violations.push({
|
|
7824
|
+
rule: 'runquery-runview-direct-setstate',
|
|
7825
|
+
severity: 'critical',
|
|
7826
|
+
line: firstArg.loc?.start.line || 0,
|
|
7827
|
+
column: firstArg.loc?.start.column || 0,
|
|
7828
|
+
message: `Passing ${resultInfo.method} result with incorrect Array.isArray check to ${funcName}. This will always pass an empty array because ${resultInfo.method} returns an object, not an array.
|
|
7829
|
+
|
|
7830
|
+
Correct pattern:
|
|
7831
|
+
if (${varName}.Success) {
|
|
7832
|
+
${funcName}(${varName}.Results || []);
|
|
7833
|
+
} else {
|
|
7834
|
+
console.error('Failed to load data:', ${varName}.ErrorMessage);
|
|
7835
|
+
${funcName}([]);
|
|
7836
|
+
}
|
|
7837
|
+
|
|
7838
|
+
// Or simpler:
|
|
7839
|
+
${funcName}(${varName}.Results || []);`,
|
|
7840
|
+
code: `${funcName}(Array.isArray(${varName}) ? ${varName} : [])`
|
|
7841
|
+
});
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
7845
|
+
// Check if passing result directly (not accessing .Results)
|
|
7846
|
+
if (t.isIdentifier(firstArg) && resultVariables.has(firstArg.name)) {
|
|
7847
|
+
const resultInfo = resultVariables.get(firstArg.name);
|
|
7848
|
+
violations.push({
|
|
7849
|
+
rule: 'runquery-runview-direct-setstate',
|
|
7850
|
+
severity: 'critical',
|
|
7851
|
+
line: firstArg.loc?.start.line || 0,
|
|
7852
|
+
column: firstArg.loc?.start.column || 0,
|
|
7853
|
+
message: `Passing ${resultInfo.method} result object directly to ${funcName}. The result is an object { Success, Results, ErrorMessage }, not the data array.
|
|
7854
|
+
|
|
7855
|
+
Correct pattern:
|
|
7856
|
+
if (${firstArg.name}.Success) {
|
|
7857
|
+
${funcName}(${firstArg.name}.Results || []);
|
|
7858
|
+
} else {
|
|
7859
|
+
console.error('Failed to load data:', ${firstArg.name}.ErrorMessage);
|
|
7860
|
+
${funcName}([]);
|
|
7861
|
+
}`,
|
|
7862
|
+
code: `${funcName}(${firstArg.name})`
|
|
7863
|
+
});
|
|
7864
|
+
}
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
});
|
|
7869
|
+
return violations;
|
|
7870
|
+
}
|
|
7871
|
+
},
|
|
7872
|
+
{
|
|
7873
|
+
name: 'styles-invalid-path',
|
|
7874
|
+
appliesTo: 'all',
|
|
7875
|
+
test: (ast, componentName, componentSpec) => {
|
|
7876
|
+
const violations = [];
|
|
7877
|
+
const analyzer = ComponentLinter.getStylesAnalyzer();
|
|
7878
|
+
(0, traverse_1.default)(ast, {
|
|
7879
|
+
MemberExpression(path) {
|
|
7880
|
+
// Build the complete property chain first
|
|
7881
|
+
let propertyChain = [];
|
|
7882
|
+
let current = path.node;
|
|
7883
|
+
// Walk up from the deepest member expression to build the full chain
|
|
7884
|
+
while (t.isMemberExpression(current)) {
|
|
7885
|
+
if (t.isIdentifier(current.property)) {
|
|
7886
|
+
propertyChain.unshift(current.property.name);
|
|
7887
|
+
}
|
|
7888
|
+
if (t.isIdentifier(current.object)) {
|
|
7889
|
+
propertyChain.unshift(current.object.name);
|
|
7890
|
+
break;
|
|
7891
|
+
}
|
|
7892
|
+
current = current.object;
|
|
7893
|
+
}
|
|
7894
|
+
// Only process if this is a styles access
|
|
7895
|
+
if (propertyChain[0] === 'styles') {
|
|
7896
|
+
// Validate the path
|
|
7897
|
+
if (!analyzer.isValidPath(propertyChain)) {
|
|
7898
|
+
const suggestions = analyzer.getSuggestionsForPath(propertyChain);
|
|
7899
|
+
const accessPath = propertyChain.join('.');
|
|
7900
|
+
let message = `Invalid styles property path: "${accessPath}"`;
|
|
7901
|
+
if (suggestions.didYouMean) {
|
|
7902
|
+
message += `\n\nDid you mean: ${suggestions.didYouMean}?`;
|
|
7903
|
+
}
|
|
7904
|
+
if (suggestions.correctPaths.length > 0) {
|
|
7905
|
+
message += `\n\nThe property "${propertyChain[propertyChain.length - 1]}" exists at:`;
|
|
7906
|
+
suggestions.correctPaths.forEach((p) => {
|
|
7907
|
+
message += `\n - ${p}`;
|
|
7908
|
+
});
|
|
7909
|
+
}
|
|
7910
|
+
if (suggestions.availableAtParent.length > 0) {
|
|
7911
|
+
const parentPath = propertyChain.slice(0, -1).join('.');
|
|
7912
|
+
message += `\n\nAvailable properties at ${parentPath}:`;
|
|
7913
|
+
message += `\n ${suggestions.availableAtParent.slice(0, 5).join(', ')}`;
|
|
7914
|
+
if (suggestions.availableAtParent.length > 5) {
|
|
7915
|
+
message += ` (and ${suggestions.availableAtParent.length - 5} more)`;
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
// Get a contextual default value
|
|
7919
|
+
const defaultValue = analyzer.getDefaultValueForPath(propertyChain);
|
|
7920
|
+
message += `\n\nSuggested fix with safe access:\n ${accessPath.replace(/\./g, '?.')} || ${defaultValue}`;
|
|
7921
|
+
violations.push({
|
|
7922
|
+
rule: 'styles-invalid-path',
|
|
7923
|
+
severity: 'critical',
|
|
7924
|
+
line: path.node.loc?.start.line || 0,
|
|
7925
|
+
column: path.node.loc?.start.column || 0,
|
|
7926
|
+
message: message,
|
|
7927
|
+
code: accessPath
|
|
7928
|
+
});
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
});
|
|
7933
|
+
return violations;
|
|
7934
|
+
}
|
|
7935
|
+
},
|
|
7936
|
+
{
|
|
7937
|
+
name: 'styles-unsafe-access',
|
|
7938
|
+
appliesTo: 'all',
|
|
7939
|
+
test: (ast, componentName, componentSpec) => {
|
|
7940
|
+
const violations = [];
|
|
7941
|
+
const analyzer = ComponentLinter.getStylesAnalyzer();
|
|
7942
|
+
(0, traverse_1.default)(ast, {
|
|
7943
|
+
MemberExpression(path) {
|
|
7944
|
+
// Build the complete property chain first
|
|
7945
|
+
let propertyChain = [];
|
|
7946
|
+
let current = path.node;
|
|
7947
|
+
let hasOptionalChaining = path.node.optional || false;
|
|
7948
|
+
// Walk up from the deepest member expression to build the full chain
|
|
7949
|
+
while (t.isMemberExpression(current)) {
|
|
7950
|
+
if (current.optional) {
|
|
7951
|
+
hasOptionalChaining = true;
|
|
7952
|
+
}
|
|
7953
|
+
if (t.isIdentifier(current.property)) {
|
|
7954
|
+
propertyChain.unshift(current.property.name);
|
|
7955
|
+
}
|
|
7956
|
+
if (t.isIdentifier(current.object)) {
|
|
7957
|
+
propertyChain.unshift(current.object.name);
|
|
7958
|
+
break;
|
|
7959
|
+
}
|
|
7960
|
+
current = current.object;
|
|
7961
|
+
}
|
|
7962
|
+
// Only process if this is a styles access
|
|
7963
|
+
if (propertyChain[0] === 'styles') {
|
|
7964
|
+
// Only check valid paths for safe access
|
|
7965
|
+
if (analyzer.isValidPath(propertyChain)) {
|
|
7966
|
+
// Check if this is a nested access without optional chaining or fallback
|
|
7967
|
+
if (propertyChain.length > 2 && !hasOptionalChaining) {
|
|
7968
|
+
// Check if there's a fallback (|| operator)
|
|
7969
|
+
const parent = path.parent;
|
|
7970
|
+
const hasFallback = t.isLogicalExpression(parent) && parent.operator === '||';
|
|
7971
|
+
if (!hasFallback) {
|
|
7972
|
+
const accessPath = propertyChain.join('.');
|
|
7973
|
+
const defaultValue = analyzer.getDefaultValueForPath(propertyChain);
|
|
7974
|
+
violations.push({
|
|
7975
|
+
rule: 'styles-unsafe-access',
|
|
7976
|
+
severity: 'high',
|
|
7977
|
+
line: path.node.loc?.start.line || 0,
|
|
7978
|
+
column: path.node.loc?.start.column || 0,
|
|
7979
|
+
message: `Unsafe styles property access: "${accessPath}". While this path is valid, you should use optional chaining for safety.
|
|
7980
|
+
|
|
7981
|
+
Example with optional chaining:
|
|
7982
|
+
${accessPath.replace(/\./g, '?.')} || ${defaultValue}
|
|
7983
|
+
|
|
7984
|
+
This prevents runtime errors if the styles object structure changes.`,
|
|
7985
|
+
code: accessPath
|
|
7986
|
+
});
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
});
|
|
7993
|
+
return violations;
|
|
7994
|
+
}
|
|
7995
|
+
},
|
|
7996
|
+
{
|
|
7997
|
+
name: 'runquery-runview-spread-operator',
|
|
7998
|
+
appliesTo: 'all',
|
|
7999
|
+
test: (ast, componentName, componentSpec) => {
|
|
8000
|
+
const violations = [];
|
|
8001
|
+
// Track variables that hold RunView/RunQuery results
|
|
8002
|
+
const resultVariables = new Map();
|
|
8003
|
+
// First pass: identify all RunView/RunQuery calls
|
|
8004
|
+
(0, traverse_1.default)(ast, {
|
|
8005
|
+
AwaitExpression(path) {
|
|
8006
|
+
const callExpr = path.node.argument;
|
|
8007
|
+
if (t.isCallExpression(callExpr) && t.isMemberExpression(callExpr.callee)) {
|
|
8008
|
+
const callee = callExpr.callee;
|
|
8009
|
+
if (t.isMemberExpression(callee.object) &&
|
|
8010
|
+
t.isIdentifier(callee.object.object) &&
|
|
8011
|
+
callee.object.object.name === 'utilities' &&
|
|
8012
|
+
t.isIdentifier(callee.object.property)) {
|
|
8013
|
+
const subObject = callee.object.property.name;
|
|
8014
|
+
const method = t.isIdentifier(callee.property) ? callee.property.name : '';
|
|
8015
|
+
let methodType = null;
|
|
8016
|
+
if (subObject === 'rv' && (method === 'RunView' || method === 'RunViews')) {
|
|
8017
|
+
methodType = method;
|
|
8018
|
+
}
|
|
8019
|
+
else if (subObject === 'rq' && method === 'RunQuery') {
|
|
8020
|
+
methodType = 'RunQuery';
|
|
8021
|
+
}
|
|
8022
|
+
if (methodType) {
|
|
8023
|
+
const parent = path.parent;
|
|
8024
|
+
if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
|
|
8025
|
+
resultVariables.set(parent.id.name, {
|
|
8026
|
+
line: parent.id.loc?.start.line || 0,
|
|
8027
|
+
column: parent.id.loc?.start.column || 0,
|
|
8028
|
+
method: methodType,
|
|
8029
|
+
varName: parent.id.name
|
|
8030
|
+
});
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
});
|
|
8037
|
+
// Second pass: check for spread operator usage
|
|
8038
|
+
(0, traverse_1.default)(ast, {
|
|
8039
|
+
SpreadElement(path) {
|
|
8040
|
+
if (t.isIdentifier(path.node.argument)) {
|
|
8041
|
+
const varName = path.node.argument.name;
|
|
8042
|
+
if (resultVariables.has(varName)) {
|
|
8043
|
+
const resultInfo = resultVariables.get(varName);
|
|
8044
|
+
violations.push({
|
|
8045
|
+
rule: 'runquery-runview-spread-operator',
|
|
8046
|
+
severity: 'critical',
|
|
8047
|
+
line: path.node.loc?.start.line || 0,
|
|
8048
|
+
column: path.node.loc?.start.column || 0,
|
|
8049
|
+
message: `Cannot use spread operator on ${resultInfo.method} result object. Use ...${varName}.Results to spread the data array.
|
|
8050
|
+
|
|
8051
|
+
Correct pattern:
|
|
8052
|
+
const allData = [...existingData, ...${varName}.Results];
|
|
8053
|
+
|
|
8054
|
+
// Or with null safety:
|
|
8055
|
+
const allData = [...existingData, ...(${varName}.Results || [])];`,
|
|
8056
|
+
code: `...${varName}`
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
});
|
|
8062
|
+
return violations;
|
|
8063
|
+
}
|
|
7142
8064
|
}
|
|
7143
8065
|
];
|
|
7144
8066
|
//# sourceMappingURL=component-linter.js.map
|