@memberjunction/react-test-harness 2.98.0 → 2.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +482 -118
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/component-runner.d.ts +2 -1
- package/dist/lib/component-runner.d.ts.map +1 -1
- package/dist/lib/component-runner.js +138 -7
- package/dist/lib/component-runner.js.map +1 -1
- package/package.json +3 -3
|
@@ -1584,34 +1584,24 @@ await utilities.rq.RunQuery({
|
|
|
1584
1584
|
break;
|
|
1585
1585
|
case 'component-props-validation':
|
|
1586
1586
|
violation.suggestion = {
|
|
1587
|
-
text: 'Components can only accept standard props and props explicitly defined in the component spec.
|
|
1587
|
+
text: 'Components can only accept standard props and props explicitly defined in the component spec. The spec is provided by the architect and cannot be modified - your code must match the spec exactly.',
|
|
1588
1588
|
example: `// ❌ WRONG - Component with undeclared props:
|
|
1589
1589
|
function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
|
|
1590
|
-
// customers, orders, selectedId are NOT
|
|
1590
|
+
// ERROR: customers, orders, selectedId are NOT in the spec
|
|
1591
|
+
// The spec defines what props are allowed - you cannot add new ones
|
|
1591
1592
|
}
|
|
1592
1593
|
|
|
1593
|
-
// ✅ CORRECT
|
|
1594
|
+
// ✅ CORRECT - Use only standard props and props defined in the spec:
|
|
1594
1595
|
function MyComponent({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {
|
|
1595
|
-
//
|
|
1596
|
+
// If you need data like customers/orders, load it internally using utilities
|
|
1596
1597
|
const [customers, setCustomers] = useState([]);
|
|
1597
1598
|
const [orders, setOrders] = useState([]);
|
|
1598
1599
|
const [selectedId, setSelectedId] = useState(savedUserSettings?.selectedId);
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
// ✅ CORRECT Option 2 - Define props in component spec:
|
|
1602
|
-
// In spec.properties array:
|
|
1603
|
-
// [
|
|
1604
|
-
// { name: "customers", type: "array", required: false, description: "Customer list" },
|
|
1605
|
-
// { name: "orders", type: "array", required: false, description: "Order list" },
|
|
1606
|
-
// { name: "selectedId", type: "string", required: false, description: "Selected item ID" }
|
|
1607
|
-
// ]
|
|
1608
|
-
// Then the component can accept them:
|
|
1609
|
-
function MyComponent({ utilities, styles, components, customers, orders, selectedId }) {
|
|
1610
|
-
// These props are now allowed because they're defined in the spec
|
|
1611
1600
|
|
|
1612
1601
|
useEffect(() => {
|
|
1613
1602
|
const loadData = async () => {
|
|
1614
1603
|
try {
|
|
1604
|
+
// Load customers data internally
|
|
1615
1605
|
const result = await utilities.rv.RunView({
|
|
1616
1606
|
EntityName: 'Customers',
|
|
1617
1607
|
Fields: ['ID', 'Name', 'Status']
|
|
@@ -1626,8 +1616,12 @@ function MyComponent({ utilities, styles, components, customers, orders, selecte
|
|
|
1626
1616
|
loadData();
|
|
1627
1617
|
}, []);
|
|
1628
1618
|
|
|
1629
|
-
return <div>{/* Use state, not props */}</div>;
|
|
1630
|
-
}
|
|
1619
|
+
return <div>{/* Use state variables, not props */}</div>;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// NOTE: If the spec DOES define additional props (e.g., customers, orders),
|
|
1623
|
+
// then you MUST accept and use them. Check the spec's properties array
|
|
1624
|
+
// to see what props are required/optional beyond the standard ones.`
|
|
1631
1625
|
};
|
|
1632
1626
|
break;
|
|
1633
1627
|
case 'runview-runquery-result-direct-usage':
|
|
@@ -2678,7 +2672,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2678
2672
|
severity: 'critical',
|
|
2679
2673
|
line: path.node.loc?.start.line || 0,
|
|
2680
2674
|
column: path.node.loc?.start.column || 0,
|
|
2681
|
-
message: `Component "${componentName}" is trying to
|
|
2675
|
+
message: `Component "${componentName}" is trying to access window.${propertyName}. Libraries must be accessed using unwrapComponents, not through the window object. If this library is in your spec, use: const { ... } = unwrapComponents(${propertyName}, [...]); If it's not in your spec, you cannot use it.`,
|
|
2682
2676
|
code: path.toString().substring(0, 100)
|
|
2683
2677
|
});
|
|
2684
2678
|
}
|
|
@@ -2916,6 +2910,66 @@ ComponentLinter.universalComponentRules = [
|
|
|
2916
2910
|
return violations;
|
|
2917
2911
|
}
|
|
2918
2912
|
},
|
|
2913
|
+
{
|
|
2914
|
+
name: 'use-unwrap-components',
|
|
2915
|
+
appliesTo: 'all',
|
|
2916
|
+
test: (ast, componentName, componentSpec) => {
|
|
2917
|
+
const violations = [];
|
|
2918
|
+
// Build a set of library global variables
|
|
2919
|
+
const libraryGlobals = new Set();
|
|
2920
|
+
if (componentSpec?.libraries) {
|
|
2921
|
+
for (const lib of componentSpec.libraries) {
|
|
2922
|
+
if (lib.globalVariable) {
|
|
2923
|
+
libraryGlobals.add(lib.globalVariable);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
(0, traverse_1.default)(ast, {
|
|
2928
|
+
VariableDeclarator(path) {
|
|
2929
|
+
// Check for direct destructuring from library globals
|
|
2930
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(path.node.init)) {
|
|
2931
|
+
const sourceVar = path.node.init.name;
|
|
2932
|
+
// Check if this is destructuring from a library global
|
|
2933
|
+
if (libraryGlobals.has(sourceVar)) {
|
|
2934
|
+
// Extract the destructured component names
|
|
2935
|
+
const componentNames = [];
|
|
2936
|
+
for (const prop of path.node.id.properties) {
|
|
2937
|
+
if (t.isObjectProperty(prop)) {
|
|
2938
|
+
if (t.isIdentifier(prop.key)) {
|
|
2939
|
+
componentNames.push(prop.key.name);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
violations.push({
|
|
2944
|
+
rule: 'use-unwrap-components',
|
|
2945
|
+
severity: 'critical',
|
|
2946
|
+
line: path.node.loc?.start.line || 0,
|
|
2947
|
+
column: path.node.loc?.start.column || 0,
|
|
2948
|
+
message: `Direct destructuring from library "${sourceVar}" is not allowed. You MUST use unwrapComponents to access library components. Replace "const { ${componentNames.join(', ')} } = ${sourceVar};" with "const { ${componentNames.join(', ')} } = unwrapComponents(${sourceVar}, [${componentNames.map(n => `'${n}'`).join(', ')}]);"`
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
// Also check for MemberExpression destructuring like const { Button } = antd.Button
|
|
2953
|
+
if (t.isObjectPattern(path.node.id) && t.isMemberExpression(path.node.init)) {
|
|
2954
|
+
const memberExpr = path.node.init;
|
|
2955
|
+
if (t.isIdentifier(memberExpr.object)) {
|
|
2956
|
+
const objName = memberExpr.object.name;
|
|
2957
|
+
if (libraryGlobals.has(objName)) {
|
|
2958
|
+
violations.push({
|
|
2959
|
+
rule: 'use-unwrap-components',
|
|
2960
|
+
severity: 'critical',
|
|
2961
|
+
line: path.node.loc?.start.line || 0,
|
|
2962
|
+
column: path.node.loc?.start.column || 0,
|
|
2963
|
+
message: `Direct destructuring from library member expression is not allowed. Use unwrapComponents to safely access library components. Example: Instead of "const { Something } = ${objName}.Something;", use "const { Something } = unwrapComponents(${objName}, ['Something']);"`
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
return violations;
|
|
2971
|
+
}
|
|
2972
|
+
},
|
|
2919
2973
|
{
|
|
2920
2974
|
name: 'library-variable-names',
|
|
2921
2975
|
appliesTo: 'all',
|
|
@@ -2948,7 +3002,7 @@ ComponentLinter.universalComponentRules = [
|
|
|
2948
3002
|
severity: 'critical',
|
|
2949
3003
|
line: path.node.loc?.start.line || 0,
|
|
2950
3004
|
column: path.node.loc?.start.column || 0,
|
|
2951
|
-
message: `Incorrect library global variable "${sourceVar}". Use
|
|
3005
|
+
message: `Incorrect library global variable "${sourceVar}". Use unwrapComponents with the correct global: "const { ... } = unwrapComponents(${correctGlobal}, [...]);"`
|
|
2952
3006
|
});
|
|
2953
3007
|
}
|
|
2954
3008
|
}
|
|
@@ -4962,6 +5016,92 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4962
5016
|
return violations;
|
|
4963
5017
|
}
|
|
4964
5018
|
},
|
|
5019
|
+
{
|
|
5020
|
+
name: 'string-replace-all-occurrences',
|
|
5021
|
+
appliesTo: 'all',
|
|
5022
|
+
test: (ast, componentName, componentSpec) => {
|
|
5023
|
+
const violations = [];
|
|
5024
|
+
// Template patterns that are HIGH severity (likely to have multiple occurrences)
|
|
5025
|
+
const templatePatterns = [
|
|
5026
|
+
{ pattern: /\{\{[^}]+\}\}/, example: '{{field}}', desc: 'double curly braces' },
|
|
5027
|
+
{ pattern: /\{[^}]+\}/, example: '{field}', desc: 'single curly braces' },
|
|
5028
|
+
{ pattern: /<<[^>]+>>/, example: '<<field>>', desc: 'double angle brackets' },
|
|
5029
|
+
{ pattern: /<[^>]+>/, example: '<field>', desc: 'single angle brackets' }
|
|
5030
|
+
];
|
|
5031
|
+
(0, traverse_1.default)(ast, {
|
|
5032
|
+
CallExpression(path) {
|
|
5033
|
+
const callee = path.node.callee;
|
|
5034
|
+
// Check if it's a .replace() method call
|
|
5035
|
+
if (t.isMemberExpression(callee) &&
|
|
5036
|
+
t.isIdentifier(callee.property) &&
|
|
5037
|
+
callee.property.name === 'replace') {
|
|
5038
|
+
const args = path.node.arguments;
|
|
5039
|
+
if (args.length >= 2) {
|
|
5040
|
+
const [searchArg, replaceArg] = args;
|
|
5041
|
+
// Handle string literal search patterns
|
|
5042
|
+
if (t.isStringLiteral(searchArg)) {
|
|
5043
|
+
const searchValue = searchArg.value;
|
|
5044
|
+
// Check if it matches any template pattern
|
|
5045
|
+
let matchedPattern = null;
|
|
5046
|
+
for (const tp of templatePatterns) {
|
|
5047
|
+
if (tp.pattern.test(searchValue)) {
|
|
5048
|
+
matchedPattern = tp;
|
|
5049
|
+
break;
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
if (matchedPattern) {
|
|
5053
|
+
// HIGH severity for template patterns
|
|
5054
|
+
violations.push({
|
|
5055
|
+
rule: 'string-replace-all-occurrences',
|
|
5056
|
+
severity: 'high',
|
|
5057
|
+
line: path.node.loc?.start.line || 0,
|
|
5058
|
+
column: path.node.loc?.start.column || 0,
|
|
5059
|
+
message: `Using replace() with ${matchedPattern.desc} template '${searchValue}' only replaces the first occurrence. This will cause bugs if the template appears multiple times.`,
|
|
5060
|
+
suggestion: {
|
|
5061
|
+
text: `Use .replaceAll('${searchValue}', ...) to replace all occurrences`,
|
|
5062
|
+
example: `str.replaceAll('${searchValue}', value)`
|
|
5063
|
+
}
|
|
5064
|
+
});
|
|
5065
|
+
}
|
|
5066
|
+
else {
|
|
5067
|
+
// LOW severity for general replace() usage
|
|
5068
|
+
violations.push({
|
|
5069
|
+
rule: 'string-replace-all-occurrences',
|
|
5070
|
+
severity: 'low',
|
|
5071
|
+
line: path.node.loc?.start.line || 0,
|
|
5072
|
+
column: path.node.loc?.start.column || 0,
|
|
5073
|
+
message: `Note: replace() only replaces the first occurrence of '${searchValue}'. If you need to replace all occurrences, use replaceAll() or a global regex.`,
|
|
5074
|
+
suggestion: {
|
|
5075
|
+
text: `Consider if you need replaceAll() instead`,
|
|
5076
|
+
example: `str.replaceAll('${searchValue}', value) or str.replace(/${searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/g, value)`
|
|
5077
|
+
}
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
// Handle regex patterns - only warn if not global
|
|
5082
|
+
else if (t.isRegExpLiteral(searchArg)) {
|
|
5083
|
+
const flags = searchArg.flags || '';
|
|
5084
|
+
if (!flags.includes('g')) {
|
|
5085
|
+
violations.push({
|
|
5086
|
+
rule: 'string-replace-all-occurrences',
|
|
5087
|
+
severity: 'low',
|
|
5088
|
+
line: path.node.loc?.start.line || 0,
|
|
5089
|
+
column: path.node.loc?.start.column || 0,
|
|
5090
|
+
message: `Regex pattern without 'g' flag only replaces first match. Add 'g' flag for global replacement.`,
|
|
5091
|
+
suggestion: {
|
|
5092
|
+
text: `Add 'g' flag to replace all matches`,
|
|
5093
|
+
example: `str.replace(/${searchArg.pattern}/${flags}g, value)`
|
|
5094
|
+
}
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
});
|
|
5102
|
+
return violations;
|
|
5103
|
+
}
|
|
5104
|
+
},
|
|
4965
5105
|
{
|
|
4966
5106
|
name: 'component-props-validation',
|
|
4967
5107
|
appliesTo: 'all',
|
|
@@ -4970,8 +5110,10 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4970
5110
|
const standardProps = new Set(['utilities', 'styles', 'components', 'callbacks', 'savedUserSettings', 'onSaveUserSettings']);
|
|
4971
5111
|
// React special props that are automatically provided by React
|
|
4972
5112
|
const reactSpecialProps = new Set(['children']);
|
|
4973
|
-
// Build set of allowed props: standard props + React special props + componentSpec properties
|
|
5113
|
+
// Build set of allowed props: standard props + React special props + componentSpec properties + events
|
|
4974
5114
|
const allowedProps = new Set([...standardProps, ...reactSpecialProps]);
|
|
5115
|
+
// Track required props separately for validation
|
|
5116
|
+
const requiredProps = new Set();
|
|
4975
5117
|
// Add props from componentSpec.properties if they exist
|
|
4976
5118
|
// These are the architect-defined props that this component is allowed to accept
|
|
4977
5119
|
const specDefinedProps = [];
|
|
@@ -4980,6 +5122,21 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4980
5122
|
if (prop.name) {
|
|
4981
5123
|
allowedProps.add(prop.name);
|
|
4982
5124
|
specDefinedProps.push(prop.name);
|
|
5125
|
+
if (prop.required) {
|
|
5126
|
+
requiredProps.add(prop.name);
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
// Add events from componentSpec.events if they exist
|
|
5132
|
+
// Events are functions passed as props to the component
|
|
5133
|
+
const specDefinedEvents = [];
|
|
5134
|
+
if (componentSpec?.events) {
|
|
5135
|
+
for (const event of componentSpec.events) {
|
|
5136
|
+
if (event.name) {
|
|
5137
|
+
allowedProps.add(event.name);
|
|
5138
|
+
specDefinedEvents.push(event.name);
|
|
5139
|
+
// Events are typically optional unless explicitly marked required
|
|
4983
5140
|
}
|
|
4984
5141
|
}
|
|
4985
5142
|
}
|
|
@@ -4999,23 +5156,36 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
4999
5156
|
}
|
|
5000
5157
|
}
|
|
5001
5158
|
}
|
|
5159
|
+
// Check for missing required props
|
|
5160
|
+
const missingRequired = Array.from(requiredProps).filter(prop => !allProps.includes(prop) && !standardProps.has(prop));
|
|
5161
|
+
// Report missing required props
|
|
5162
|
+
if (missingRequired.length > 0) {
|
|
5163
|
+
violations.push({
|
|
5164
|
+
rule: 'component-props-validation',
|
|
5165
|
+
severity: 'critical',
|
|
5166
|
+
line: path.node.loc?.start.line || 0,
|
|
5167
|
+
column: path.node.loc?.start.column || 0,
|
|
5168
|
+
message: `Component "${componentName}" is missing required props: ${missingRequired.join(', ')}. These props are marked as required in the component specification.`
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5002
5171
|
// Only report if there are non-allowed props
|
|
5003
5172
|
if (invalidProps.length > 0) {
|
|
5004
5173
|
let message;
|
|
5005
|
-
if (specDefinedProps.length > 0) {
|
|
5174
|
+
if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
|
|
5006
5175
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5007
5176
|
`This component can only accept: ` +
|
|
5008
5177
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5009
|
-
`(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
|
|
5010
|
-
`(3)
|
|
5011
|
-
`
|
|
5178
|
+
(specDefinedProps.length > 0 ? `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` : '') +
|
|
5179
|
+
(specDefinedEvents.length > 0 ? `(3) Spec-defined events: ${specDefinedEvents.join(', ')}, ` : '') +
|
|
5180
|
+
`(4) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5181
|
+
`Any additional props must be defined in the component spec's properties or events array.`;
|
|
5012
5182
|
}
|
|
5013
5183
|
else {
|
|
5014
5184
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5015
5185
|
`This component can only accept: ` +
|
|
5016
5186
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5017
5187
|
`(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5018
|
-
`To accept additional props, they must be defined in the component spec's properties array.`;
|
|
5188
|
+
`To accept additional props, they must be defined in the component spec's properties or events array.`;
|
|
5019
5189
|
}
|
|
5020
5190
|
violations.push({
|
|
5021
5191
|
rule: 'component-props-validation',
|
|
@@ -5046,28 +5216,41 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5046
5216
|
}
|
|
5047
5217
|
}
|
|
5048
5218
|
}
|
|
5219
|
+
// Check for missing required props
|
|
5220
|
+
const missingRequired = Array.from(requiredProps).filter(prop => !allProps.includes(prop) && !standardProps.has(prop));
|
|
5221
|
+
// Report missing required props
|
|
5222
|
+
if (missingRequired.length > 0) {
|
|
5223
|
+
violations.push({
|
|
5224
|
+
rule: 'component-props-validation',
|
|
5225
|
+
severity: 'critical',
|
|
5226
|
+
line: init.loc?.start.line || 0,
|
|
5227
|
+
column: init.loc?.start.column || 0,
|
|
5228
|
+
message: `Component "${componentName}" is missing required props: ${missingRequired.join(', ')}. These props are marked as required in the component specification.`
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5049
5231
|
if (invalidProps.length > 0) {
|
|
5050
5232
|
let message;
|
|
5051
|
-
if (specDefinedProps.length > 0) {
|
|
5233
|
+
if (specDefinedProps.length > 0 || specDefinedEvents.length > 0) {
|
|
5052
5234
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5053
5235
|
`This component can only accept: ` +
|
|
5054
5236
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5055
|
-
`(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` +
|
|
5056
|
-
`(3)
|
|
5057
|
-
`
|
|
5237
|
+
(specDefinedProps.length > 0 ? `(2) Spec-defined props: ${specDefinedProps.join(', ')}, ` : '') +
|
|
5238
|
+
(specDefinedEvents.length > 0 ? `(3) Spec-defined events: ${specDefinedEvents.join(', ')}, ` : '') +
|
|
5239
|
+
`(4) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5240
|
+
`Any additional props must be defined in the component spec's properties or events array.`;
|
|
5058
5241
|
}
|
|
5059
5242
|
else {
|
|
5060
5243
|
message = `Component "${componentName}" accepts undeclared props: ${invalidProps.join(', ')}. ` +
|
|
5061
5244
|
`This component can only accept: ` +
|
|
5062
5245
|
`(1) Standard props: ${Array.from(standardProps).join(', ')}, ` +
|
|
5063
5246
|
`(2) React props: ${Array.from(reactSpecialProps).join(', ')}. ` +
|
|
5064
|
-
`To accept additional props, they must be defined in the component spec's properties array.`;
|
|
5247
|
+
`To accept additional props, they must be defined in the component spec's properties or events array.`;
|
|
5065
5248
|
}
|
|
5066
5249
|
violations.push({
|
|
5067
5250
|
rule: 'component-props-validation',
|
|
5068
5251
|
severity: 'critical',
|
|
5069
|
-
line:
|
|
5070
|
-
column:
|
|
5252
|
+
line: init.loc?.start.line || 0,
|
|
5253
|
+
column: init.loc?.start.column || 0,
|
|
5071
5254
|
message
|
|
5072
5255
|
});
|
|
5073
5256
|
}
|
|
@@ -5668,7 +5851,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5668
5851
|
severity: 'critical',
|
|
5669
5852
|
line: openingElement.loc?.start.line || 0,
|
|
5670
5853
|
column: openingElement.loc?.start.column || 0,
|
|
5671
|
-
message: `JSX component "${tagName}" is not defined. This looks like it should be
|
|
5854
|
+
message: `JSX component "${tagName}" is not defined. This looks like it should be from the ${libraryNames[0]} library. Add: const { ${tagName} } = unwrapComponents(${libraryNames[0]}, ['${tagName}']); at the top of your component function.`,
|
|
5672
5855
|
code: `<${tagName} ... />`
|
|
5673
5856
|
});
|
|
5674
5857
|
} else {
|
|
@@ -5678,7 +5861,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5678
5861
|
severity: 'critical',
|
|
5679
5862
|
line: openingElement.loc?.start.line || 0,
|
|
5680
5863
|
column: openingElement.loc?.start.column || 0,
|
|
5681
|
-
message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}.
|
|
5864
|
+
message: `JSX component "${tagName}" is not defined. Available libraries: ${libraryNames.join(', ')}. Use unwrapComponents to access it: const { ${tagName} } = unwrapComponents(LibraryName, ['${tagName}']);`,
|
|
5682
5865
|
code: `<${tagName} ... />`
|
|
5683
5866
|
});
|
|
5684
5867
|
}
|
|
@@ -5711,7 +5894,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5711
5894
|
severity: 'high',
|
|
5712
5895
|
line: openingElement.loc?.start.line || 0,
|
|
5713
5896
|
column: openingElement.loc?.start.column || 0,
|
|
5714
|
-
message: `JSX component "${tagName}" is not defined.
|
|
5897
|
+
message: `JSX component "${tagName}" is not defined. You must either: (1) define it in your component, (2) use a component that's already in the spec's dependencies, or (3) destructure it from a library that's already in the spec's libraries.`,
|
|
5715
5898
|
code: `<${tagName} ... />`
|
|
5716
5899
|
});
|
|
5717
5900
|
}
|
|
@@ -5739,42 +5922,13 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5739
5922
|
appliesTo: 'all',
|
|
5740
5923
|
test: (ast, componentName, componentSpec) => {
|
|
5741
5924
|
const violations = [];
|
|
5742
|
-
//
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
if (componentSpec?.dataRequirements) {
|
|
5746
|
-
// Handle queries in different possible locations
|
|
5747
|
-
if (Array.isArray(componentSpec.dataRequirements)) {
|
|
5748
|
-
// If it's an array directly
|
|
5749
|
-
componentSpec.dataRequirements.forEach((req) => {
|
|
5750
|
-
if (req.type === 'query' && req.name) {
|
|
5751
|
-
declaredQueries.add(req.name.toLowerCase());
|
|
5752
|
-
}
|
|
5753
|
-
if (req.type === 'entity' && req.name) {
|
|
5754
|
-
declaredEntities.add(req.name.toLowerCase());
|
|
5755
|
-
}
|
|
5756
|
-
});
|
|
5757
|
-
}
|
|
5758
|
-
else if (typeof componentSpec.dataRequirements === 'object') {
|
|
5759
|
-
// If it's an object with queries/entities properties
|
|
5760
|
-
if (componentSpec.dataRequirements.queries) {
|
|
5761
|
-
componentSpec.dataRequirements.queries.forEach((q) => {
|
|
5762
|
-
if (q.name)
|
|
5763
|
-
declaredQueries.add(q.name.toLowerCase());
|
|
5764
|
-
});
|
|
5765
|
-
}
|
|
5766
|
-
if (componentSpec.dataRequirements.entities) {
|
|
5767
|
-
componentSpec.dataRequirements.entities.forEach((e) => {
|
|
5768
|
-
if (e.name)
|
|
5769
|
-
declaredEntities.add(e.name.toLowerCase());
|
|
5770
|
-
});
|
|
5771
|
-
}
|
|
5772
|
-
}
|
|
5773
|
-
}
|
|
5925
|
+
// NOTE: Entity/Query name validation removed from this rule to avoid duplication
|
|
5926
|
+
// The 'data-requirements-validation' rule handles comprehensive entity/query validation
|
|
5927
|
+
// This rule now focuses on RunQuery/RunView specific issues like SQL injection
|
|
5774
5928
|
(0, traverse_1.default)(ast, {
|
|
5775
5929
|
CallExpression(path) {
|
|
5776
5930
|
const callee = path.node.callee;
|
|
5777
|
-
// Check for RunQuery calls
|
|
5931
|
+
// Check for RunQuery calls - focus on SQL injection detection
|
|
5778
5932
|
if (t.isMemberExpression(callee) &&
|
|
5779
5933
|
t.isIdentifier(callee.property) &&
|
|
5780
5934
|
callee.property.name === 'RunQuery') {
|
|
@@ -5804,20 +5958,9 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5804
5958
|
code: value.value.substring(0, 100)
|
|
5805
5959
|
});
|
|
5806
5960
|
}
|
|
5807
|
-
else if (declaredQueries.size > 0 && !declaredQueries.has(queryName.toLowerCase())) {
|
|
5808
|
-
// Only validate if we have declared queries
|
|
5809
|
-
violations.push({
|
|
5810
|
-
rule: 'runquery-runview-validation',
|
|
5811
|
-
severity: 'high',
|
|
5812
|
-
line: value.loc?.start.line || 0,
|
|
5813
|
-
column: value.loc?.start.column || 0,
|
|
5814
|
-
message: `Query "${queryName}" is not declared in dataRequirements.queries. Available queries: ${Array.from(declaredQueries).join(', ')}`,
|
|
5815
|
-
code: path.toString().substring(0, 100)
|
|
5816
|
-
});
|
|
5817
|
-
}
|
|
5818
5961
|
}
|
|
5819
5962
|
else if (t.isIdentifier(value) || t.isTemplateLiteral(value)) {
|
|
5820
|
-
// Dynamic query name -
|
|
5963
|
+
// Dynamic query name - warn that it shouldn't be SQL
|
|
5821
5964
|
violations.push({
|
|
5822
5965
|
rule: 'runquery-runview-validation',
|
|
5823
5966
|
severity: 'medium',
|
|
@@ -5830,43 +5973,7 @@ Valid properties: QueryID, QueryName, CategoryID, CategoryPath, Parameters, MaxR
|
|
|
5830
5973
|
}
|
|
5831
5974
|
}
|
|
5832
5975
|
}
|
|
5833
|
-
//
|
|
5834
|
-
if (t.isMemberExpression(callee) &&
|
|
5835
|
-
t.isIdentifier(callee.property) &&
|
|
5836
|
-
(callee.property.name === 'RunView' || callee.property.name === 'RunViews')) {
|
|
5837
|
-
const args = path.node.arguments;
|
|
5838
|
-
// Handle both single object and array of objects
|
|
5839
|
-
const checkEntityName = (objExpr) => {
|
|
5840
|
-
const entityNameProp = objExpr.properties.find(p => t.isObjectProperty(p) &&
|
|
5841
|
-
t.isIdentifier(p.key) &&
|
|
5842
|
-
p.key.name === 'EntityName');
|
|
5843
|
-
if (entityNameProp && t.isObjectProperty(entityNameProp) && t.isStringLiteral(entityNameProp.value)) {
|
|
5844
|
-
const entityName = entityNameProp.value.value;
|
|
5845
|
-
if (declaredEntities.size > 0 && !declaredEntities.has(entityName.toLowerCase())) {
|
|
5846
|
-
violations.push({
|
|
5847
|
-
rule: 'runquery-runview-validation',
|
|
5848
|
-
severity: 'high',
|
|
5849
|
-
line: entityNameProp.value.loc?.start.line || 0,
|
|
5850
|
-
column: entityNameProp.value.loc?.start.column || 0,
|
|
5851
|
-
message: `Entity "${entityName}" is not declared in dataRequirements.entities. Available entities: ${Array.from(declaredEntities).join(', ')}`,
|
|
5852
|
-
code: path.toString().substring(0, 100)
|
|
5853
|
-
});
|
|
5854
|
-
}
|
|
5855
|
-
}
|
|
5856
|
-
};
|
|
5857
|
-
if (args.length > 0) {
|
|
5858
|
-
if (t.isObjectExpression(args[0])) {
|
|
5859
|
-
checkEntityName(args[0]);
|
|
5860
|
-
}
|
|
5861
|
-
else if (t.isArrayExpression(args[0])) {
|
|
5862
|
-
args[0].elements.forEach(elem => {
|
|
5863
|
-
if (t.isObjectExpression(elem)) {
|
|
5864
|
-
checkEntityName(elem);
|
|
5865
|
-
}
|
|
5866
|
-
});
|
|
5867
|
-
}
|
|
5868
|
-
}
|
|
5869
|
-
}
|
|
5976
|
+
// RunView validation removed - handled by data-requirements-validation
|
|
5870
5977
|
}
|
|
5871
5978
|
});
|
|
5872
5979
|
return violations;
|
|
@@ -8096,6 +8203,263 @@ const [state, setState] = useState(initialValue);`
|
|
|
8096
8203
|
});
|
|
8097
8204
|
return violations;
|
|
8098
8205
|
}
|
|
8206
|
+
},
|
|
8207
|
+
{
|
|
8208
|
+
name: 'callbacks-usage-validation',
|
|
8209
|
+
appliesTo: 'all',
|
|
8210
|
+
test: (ast, componentName, componentSpec) => {
|
|
8211
|
+
const violations = [];
|
|
8212
|
+
// Define the allowed methods on ComponentCallbacks interface
|
|
8213
|
+
const allowedCallbackMethods = new Set(['OpenEntityRecord', 'RegisterMethod']);
|
|
8214
|
+
// Build list of component's event names from spec
|
|
8215
|
+
const componentEvents = new Set();
|
|
8216
|
+
if (componentSpec?.events) {
|
|
8217
|
+
for (const event of componentSpec.events) {
|
|
8218
|
+
if (event.name) {
|
|
8219
|
+
componentEvents.add(event.name);
|
|
8220
|
+
}
|
|
8221
|
+
}
|
|
8222
|
+
}
|
|
8223
|
+
(0, traverse_1.default)(ast, {
|
|
8224
|
+
MemberExpression(path) {
|
|
8225
|
+
// Check for callbacks.something access
|
|
8226
|
+
if (t.isIdentifier(path.node.object) && path.node.object.name === 'callbacks') {
|
|
8227
|
+
if (t.isIdentifier(path.node.property)) {
|
|
8228
|
+
const methodName = path.node.property.name;
|
|
8229
|
+
// Check if it's trying to access an event
|
|
8230
|
+
if (componentEvents.has(methodName)) {
|
|
8231
|
+
violations.push({
|
|
8232
|
+
rule: 'callbacks-usage-validation',
|
|
8233
|
+
severity: 'critical',
|
|
8234
|
+
line: path.node.loc?.start.line || 0,
|
|
8235
|
+
column: path.node.loc?.start.column || 0,
|
|
8236
|
+
message: `Event "${methodName}" should not be accessed from callbacks. Events are passed as direct props to the component. Use the prop directly: ${methodName}`,
|
|
8237
|
+
suggestion: {
|
|
8238
|
+
text: `Events defined in the component spec are passed as direct props, not through callbacks. Access the event directly as a prop.`,
|
|
8239
|
+
example: `// ❌ WRONG - Accessing event from callbacks
|
|
8240
|
+
const { ${methodName} } = callbacks || {};
|
|
8241
|
+
callbacks?.${methodName}?.(data);
|
|
8242
|
+
|
|
8243
|
+
// ✅ CORRECT - Event is a direct prop
|
|
8244
|
+
// In the component props destructuring:
|
|
8245
|
+
function MyComponent({ ..., ${methodName} }) {
|
|
8246
|
+
// Use with null checking:
|
|
8247
|
+
if (${methodName}) {
|
|
8248
|
+
${methodName}(data);
|
|
8249
|
+
}
|
|
8250
|
+
}`
|
|
8251
|
+
}
|
|
8252
|
+
});
|
|
8253
|
+
}
|
|
8254
|
+
else if (!allowedCallbackMethods.has(methodName)) {
|
|
8255
|
+
// It's not an allowed callback method
|
|
8256
|
+
violations.push({
|
|
8257
|
+
rule: 'callbacks-usage-validation',
|
|
8258
|
+
severity: 'critical',
|
|
8259
|
+
line: path.node.loc?.start.line || 0,
|
|
8260
|
+
column: path.node.loc?.start.column || 0,
|
|
8261
|
+
message: `Invalid callback method "${methodName}". The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
|
|
8262
|
+
suggestion: {
|
|
8263
|
+
text: `The callbacks prop is reserved for specific MemberJunction framework methods. Custom events should be defined in the component spec's events array and passed as props.`,
|
|
8264
|
+
example: `// Allowed callbacks methods:
|
|
8265
|
+
callbacks?.OpenEntityRecord?.(entityName, key);
|
|
8266
|
+
callbacks?.RegisterMethod?.(methodName, handler);
|
|
8267
|
+
|
|
8268
|
+
// For custom events, define them in the spec and use as props:
|
|
8269
|
+
function MyComponent({ onCustomEvent }) {
|
|
8270
|
+
if (onCustomEvent) {
|
|
8271
|
+
onCustomEvent(data);
|
|
8272
|
+
}
|
|
8273
|
+
}`
|
|
8274
|
+
}
|
|
8275
|
+
});
|
|
8276
|
+
}
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
},
|
|
8280
|
+
// Also check for destructuring from callbacks
|
|
8281
|
+
VariableDeclarator(path) {
|
|
8282
|
+
if (t.isObjectPattern(path.node.id) &&
|
|
8283
|
+
t.isIdentifier(path.node.init) &&
|
|
8284
|
+
path.node.init.name === 'callbacks') {
|
|
8285
|
+
// Check each destructured property
|
|
8286
|
+
for (const prop of path.node.id.properties) {
|
|
8287
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
8288
|
+
const methodName = prop.key.name;
|
|
8289
|
+
if (componentEvents.has(methodName)) {
|
|
8290
|
+
violations.push({
|
|
8291
|
+
rule: 'callbacks-usage-validation',
|
|
8292
|
+
severity: 'critical',
|
|
8293
|
+
line: prop.loc?.start.line || 0,
|
|
8294
|
+
column: prop.loc?.start.column || 0,
|
|
8295
|
+
message: `Event "${methodName}" should not be destructured from callbacks. Events are passed as direct props to the component.`,
|
|
8296
|
+
suggestion: {
|
|
8297
|
+
text: `Events should be destructured from the component props, not from callbacks.`,
|
|
8298
|
+
example: `// ❌ WRONG
|
|
8299
|
+
const { ${methodName} } = callbacks || {};
|
|
8300
|
+
|
|
8301
|
+
// ✅ CORRECT
|
|
8302
|
+
function MyComponent({ utilities, styles, callbacks, ${methodName} }) {
|
|
8303
|
+
// ${methodName} is now available as a prop
|
|
8304
|
+
}`
|
|
8305
|
+
}
|
|
8306
|
+
});
|
|
8307
|
+
}
|
|
8308
|
+
else if (!allowedCallbackMethods.has(methodName)) {
|
|
8309
|
+
violations.push({
|
|
8310
|
+
rule: 'callbacks-usage-validation',
|
|
8311
|
+
severity: 'critical',
|
|
8312
|
+
line: prop.loc?.start.line || 0,
|
|
8313
|
+
column: prop.loc?.start.column || 0,
|
|
8314
|
+
message: `Invalid callback method "${methodName}" being destructured. The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
|
|
8315
|
+
});
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
// Also check for: const { something } = callbacks || {}
|
|
8321
|
+
if (t.isObjectPattern(path.node.id) &&
|
|
8322
|
+
t.isLogicalExpression(path.node.init) &&
|
|
8323
|
+
path.node.init.operator === '||' &&
|
|
8324
|
+
t.isIdentifier(path.node.init.left) &&
|
|
8325
|
+
path.node.init.left.name === 'callbacks') {
|
|
8326
|
+
// Check each destructured property
|
|
8327
|
+
for (const prop of path.node.id.properties) {
|
|
8328
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
8329
|
+
const methodName = prop.key.name;
|
|
8330
|
+
if (componentEvents.has(methodName)) {
|
|
8331
|
+
violations.push({
|
|
8332
|
+
rule: 'callbacks-usage-validation',
|
|
8333
|
+
severity: 'critical',
|
|
8334
|
+
line: prop.loc?.start.line || 0,
|
|
8335
|
+
column: prop.loc?.start.column || 0,
|
|
8336
|
+
message: `Event "${methodName}" should not be destructured from callbacks. Events are passed as direct props to the component.`,
|
|
8337
|
+
suggestion: {
|
|
8338
|
+
text: `Events should be destructured from the component props, not from callbacks.`,
|
|
8339
|
+
example: `// ❌ WRONG
|
|
8340
|
+
const { ${methodName} } = callbacks || {};
|
|
8341
|
+
|
|
8342
|
+
// ✅ CORRECT
|
|
8343
|
+
function MyComponent({ utilities, styles, callbacks, ${methodName} }) {
|
|
8344
|
+
// ${methodName} is now available as a prop
|
|
8345
|
+
}`
|
|
8346
|
+
}
|
|
8347
|
+
});
|
|
8348
|
+
}
|
|
8349
|
+
else if (!allowedCallbackMethods.has(methodName)) {
|
|
8350
|
+
violations.push({
|
|
8351
|
+
rule: 'callbacks-usage-validation',
|
|
8352
|
+
severity: 'critical',
|
|
8353
|
+
line: prop.loc?.start.line || 0,
|
|
8354
|
+
column: prop.loc?.start.column || 0,
|
|
8355
|
+
message: `Invalid callback method "${methodName}" being destructured. The callbacks prop only supports: ${Array.from(allowedCallbackMethods).join(', ')}`,
|
|
8356
|
+
});
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
});
|
|
8363
|
+
return violations;
|
|
8364
|
+
}
|
|
8365
|
+
},
|
|
8366
|
+
{
|
|
8367
|
+
name: 'event-invocation-pattern',
|
|
8368
|
+
appliesTo: 'all',
|
|
8369
|
+
test: (ast, componentName, componentSpec) => {
|
|
8370
|
+
const violations = [];
|
|
8371
|
+
// Build list of component's event names from spec
|
|
8372
|
+
const componentEvents = new Set();
|
|
8373
|
+
if (componentSpec?.events) {
|
|
8374
|
+
for (const event of componentSpec.events) {
|
|
8375
|
+
if (event.name) {
|
|
8376
|
+
componentEvents.add(event.name);
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
// If no events defined, skip this rule
|
|
8381
|
+
if (componentEvents.size === 0) {
|
|
8382
|
+
return violations;
|
|
8383
|
+
}
|
|
8384
|
+
(0, traverse_1.default)(ast, {
|
|
8385
|
+
CallExpression(path) {
|
|
8386
|
+
// Check if calling an event without null checking
|
|
8387
|
+
if (t.isIdentifier(path.node.callee)) {
|
|
8388
|
+
const eventName = path.node.callee.name;
|
|
8389
|
+
if (componentEvents.has(eventName)) {
|
|
8390
|
+
// Check if this call is inside a conditional that checks for the event
|
|
8391
|
+
let hasNullCheck = false;
|
|
8392
|
+
let currentPath = path.parentPath;
|
|
8393
|
+
// Walk up the tree to see if we're inside an if statement that checks this event
|
|
8394
|
+
while (currentPath && !hasNullCheck) {
|
|
8395
|
+
if (t.isIfStatement(currentPath.node)) {
|
|
8396
|
+
const test = currentPath.node.test;
|
|
8397
|
+
// Check if the test checks for the event (simple cases)
|
|
8398
|
+
if (t.isIdentifier(test) && test.name === eventName) {
|
|
8399
|
+
hasNullCheck = true;
|
|
8400
|
+
}
|
|
8401
|
+
else if (t.isLogicalExpression(test) && test.operator === '&&') {
|
|
8402
|
+
// Check for patterns like: eventName && ...
|
|
8403
|
+
if (t.isIdentifier(test.left) && test.left.name === eventName) {
|
|
8404
|
+
hasNullCheck = true;
|
|
8405
|
+
}
|
|
8406
|
+
}
|
|
8407
|
+
}
|
|
8408
|
+
else if (t.isLogicalExpression(currentPath.node) && currentPath.node.operator === '&&') {
|
|
8409
|
+
// Check for inline conditional: eventName && eventName()
|
|
8410
|
+
if (t.isIdentifier(currentPath.node.left) && currentPath.node.left.name === eventName) {
|
|
8411
|
+
hasNullCheck = true;
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
else if (t.isConditionalExpression(currentPath.node)) {
|
|
8415
|
+
// Check for ternary: eventName ? eventName() : null
|
|
8416
|
+
if (t.isIdentifier(currentPath.node.test) && currentPath.node.test.name === eventName) {
|
|
8417
|
+
hasNullCheck = true;
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
8420
|
+
currentPath = currentPath.parentPath || null;
|
|
8421
|
+
}
|
|
8422
|
+
if (!hasNullCheck) {
|
|
8423
|
+
violations.push({
|
|
8424
|
+
rule: 'event-invocation-pattern',
|
|
8425
|
+
severity: 'medium',
|
|
8426
|
+
line: path.node.loc?.start.line || 0,
|
|
8427
|
+
column: path.node.loc?.start.column || 0,
|
|
8428
|
+
message: `Event "${eventName}" is being invoked without null-checking. Events are optional props and should be checked before invocation.`,
|
|
8429
|
+
suggestion: {
|
|
8430
|
+
text: `Always check that an event prop exists before invoking it, as events are optional.`,
|
|
8431
|
+
example: `// ❌ WRONG - No null check
|
|
8432
|
+
${eventName}(data);
|
|
8433
|
+
|
|
8434
|
+
// ✅ CORRECT - With null check
|
|
8435
|
+
if (${eventName}) {
|
|
8436
|
+
${eventName}(data);
|
|
8437
|
+
}
|
|
8438
|
+
|
|
8439
|
+
// ✅ ALSO CORRECT - Inline check
|
|
8440
|
+
${eventName} && ${eventName}(data);
|
|
8441
|
+
|
|
8442
|
+
// ✅ ALSO CORRECT - Optional chaining
|
|
8443
|
+
${eventName}?.(data);`
|
|
8444
|
+
}
|
|
8445
|
+
});
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
}
|
|
8449
|
+
},
|
|
8450
|
+
// Check for optional chaining on events (this is good!)
|
|
8451
|
+
OptionalCallExpression(path) {
|
|
8452
|
+
if (t.isIdentifier(path.node.callee)) {
|
|
8453
|
+
const eventName = path.node.callee.name;
|
|
8454
|
+
if (componentEvents.has(eventName)) {
|
|
8455
|
+
// This is actually the correct pattern, no violation
|
|
8456
|
+
return;
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8460
|
+
});
|
|
8461
|
+
return violations;
|
|
8462
|
+
}
|
|
8099
8463
|
}
|
|
8100
8464
|
];
|
|
8101
8465
|
//# sourceMappingURL=component-linter.js.map
|