x-fidelity 3.13.0 → 3.14.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/CHANGELOG.md +20 -0
- package/dist/core/configManager.test.js +2 -1
- package/dist/core/engine/engineRunner.js +1 -1
- package/dist/core/engine/engineRunner.test.js +1 -0
- package/dist/core/pluginRegistry.test.js +1 -0
- package/dist/demoConfig/node-fullstack-exemptions/project1-node-fullstack-exemptions.json +2 -2
- package/dist/demoConfig/rules/functionComplexity-iterative-rule.json +3 -3
- package/dist/demoConfig/rules/functionCount-iterative-rule.json +2 -2
- package/dist/facts/globalFileAnalysisFacts.test.js +1 -0
- package/dist/facts/index.test.js +3 -1
- package/dist/facts/openaiAnalysisFacts.test.js +1 -0
- package/dist/index.js +21 -2
- package/dist/notifications/notificationManager.js +2 -2
- package/dist/operators/fileContains.test.js +4 -1
- package/dist/operators/globalPatternCount.js +1 -1
- package/dist/operators/globalPatternCount.test.js +2 -0
- package/dist/operators/globalPatternRatio.js +1 -1
- package/dist/operators/globalPatternRatio.test.js +2 -0
- package/dist/operators/openaiAnalysisHighSeverity.test.js +2 -0
- package/dist/operators/regexMatch.test.js +4 -1
- package/dist/plugins/xfiPluginAst/facts/functionComplexityFact.js +52 -13
- package/dist/plugins/xfiPluginReactPatterns/facts/effectCleanupFact.d.ts +2 -0
- package/dist/plugins/xfiPluginReactPatterns/facts/effectCleanupFact.js +99 -0
- package/dist/plugins/xfiPluginReactPatterns/facts/hookDependencyFact.d.ts +2 -0
- package/dist/plugins/xfiPluginReactPatterns/facts/hookDependencyFact.js +102 -0
- package/dist/plugins/xfiPluginReactPatterns/index.d.ts +1 -0
- package/dist/plugins/xfiPluginReactPatterns/index.js +5 -0
- package/dist/plugins/xfiPluginReactPatterns/xfiPluginReactPatterns.d.ts +3 -0
- package/dist/plugins/xfiPluginReactPatterns/xfiPluginReactPatterns.js +16 -0
- package/dist/plugins/xfiPluginRemoteStringValidator/facts/remoteSubstringValidation.test.js +2 -0
- package/dist/plugins/xfiPluginRemoteStringValidator/operators/invalidRemoteValidation.test.js +2 -0
- package/dist/plugins/xfiPluginRequiredFiles/operators/missingRequiredFiles.test.js +4 -1
- package/dist/server/cacheManager.test.js +2 -1
- package/dist/utils/exemptionUtils.js +15 -4
- package/dist/utils/exemptionUtils.test.js +1 -0
- package/dist/utils/ruleUtils.test.js +1 -0
- package/dist/utils/telemetry.js +1 -1
- package/dist/xfidelity +21 -2
- package/package.json +4 -6
- package/src/IDEAS.md +294 -84
- package/src/core/configManager.test.ts +2 -1
- package/src/core/engine/engineRunner.test.ts +1 -0
- package/src/core/engine/engineRunner.ts +1 -1
- package/src/core/pluginRegistry.test.ts +1 -0
- package/src/demoConfig/node-fullstack-exemptions/project1-node-fullstack-exemptions.json +2 -2
- package/src/demoConfig/rules/functionComplexity-iterative-rule.json +3 -3
- package/src/demoConfig/rules/functionCount-iterative-rule.json +2 -2
- package/src/exampleTriggerFiles/mixedUIComponents.tsx +9 -9
- package/src/facts/globalFileAnalysisFacts.test.ts +1 -0
- package/src/facts/index.test.ts +3 -1
- package/src/facts/openaiAnalysisFacts.test.ts +1 -0
- package/src/index.ts +31 -2
- package/src/notifications/notificationManager.ts +2 -2
- package/src/operators/fileContains.test.ts +4 -1
- package/src/operators/globalPatternCount.test.ts +2 -0
- package/src/operators/globalPatternCount.ts +1 -1
- package/src/operators/globalPatternRatio.test.ts +2 -0
- package/src/operators/globalPatternRatio.ts +1 -1
- package/src/operators/openaiAnalysisHighSeverity.test.ts +2 -0
- package/src/operators/regexMatch.test.ts +4 -1
- package/src/plugins/xfiPluginAst/facts/functionComplexityFact.ts +67 -14
- package/src/plugins/xfiPluginReactPatterns/facts/effectCleanupFact.ts +103 -0
- package/src/plugins/xfiPluginReactPatterns/facts/hookDependencyFact.ts +103 -0
- package/src/plugins/xfiPluginReactPatterns/index.ts +1 -0
- package/src/plugins/xfiPluginReactPatterns/sampleRules/effectCleanup-iterative-rule.json +30 -0
- package/src/plugins/xfiPluginReactPatterns/sampleRules/hookDependency-iterative-rule.json +30 -0
- package/src/plugins/xfiPluginReactPatterns/xfiPluginReactPatterns.ts +16 -0
- package/src/plugins/xfiPluginRemoteStringValidator/facts/remoteSubstringValidation.test.ts +2 -0
- package/src/plugins/xfiPluginRemoteStringValidator/operators/invalidRemoteValidation.test.ts +2 -0
- package/src/plugins/xfiPluginRemoteStringValidator/sampleRules/xfiTestMatch.json +0 -6
- package/src/plugins/xfiPluginRequiredFiles/operators/missingRequiredFiles.test.ts +4 -1
- package/src/server/cacheManager.test.ts +2 -1
- package/src/utils/exemptionUtils.test.ts +1 -0
- package/src/utils/exemptionUtils.ts +23 -5
- package/src/utils/ruleUtils.test.ts +1 -0
- package/src/utils/telemetry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# [3.14.0](https://github.com/zotoio/x-fidelity/compare/v3.13.1...v3.14.0) (2025-03-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **demo:** config speed improvement ([a1be883](https://github.com/zotoio/x-fidelity/commit/a1be88367a606187b90d27af09aebb725753ed77))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add React patterns plugin with hook dependency and cleanup analysis ([7cfd38d](https://github.com/zotoio/x-fidelity/commit/7cfd38d86596ba97536aa0c2028ee054103630dd))
|
|
12
|
+
* enhance function complexity analysis with additional metrics ([a3d61dc](https://github.com/zotoio/x-fidelity/commit/a3d61dc4cd0d5382ffa13a9565cd909782aa9aa4))
|
|
13
|
+
|
|
14
|
+
## [3.13.1](https://github.com/zotoio/x-fidelity/compare/v3.13.0...v3.13.1) (2025-03-11)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **exemptions:** trace logging ([343744c](https://github.com/zotoio/x-fidelity/commit/343744c161d2756d9fa5e419426d0b4f1a37d1d4))
|
|
20
|
+
|
|
1
21
|
# [3.13.0](https://github.com/zotoio/x-fidelity/compare/v3.12.1...v3.13.0) (2025-03-10)
|
|
2
22
|
|
|
3
23
|
|
|
@@ -43,7 +43,7 @@ function runEngineOnFiles(params) {
|
|
|
43
43
|
try {
|
|
44
44
|
const { results } = yield engine.run(facts);
|
|
45
45
|
for (const result of results) {
|
|
46
|
-
logger_1.logger.
|
|
46
|
+
logger_1.logger.trace(JSON.stringify(result));
|
|
47
47
|
if (result.result) {
|
|
48
48
|
fileFailures.push({
|
|
49
49
|
ruleFailure: result.name,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"repoUrl": "git@github.com:zotoio/x-fidelity.git",
|
|
4
|
-
"rule": "
|
|
5
|
-
"expirationDate": "
|
|
4
|
+
"rule": "noDatabases-iterative",
|
|
5
|
+
"expirationDate": "2026-01-04",
|
|
6
6
|
"reason": "Upgrading dependencies is scheduled for Q4 2024"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"fact": "functionComplexity",
|
|
13
13
|
"params": {
|
|
14
14
|
"resultFact": "complexityResult",
|
|
15
|
-
"minimumComplexityLogged":
|
|
15
|
+
"minimumComplexityLogged": 25
|
|
16
16
|
},
|
|
17
17
|
"operator": "astComplexity",
|
|
18
|
-
"value":
|
|
18
|
+
"value": 40
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
22
|
"event": {
|
|
23
23
|
"type": "warning",
|
|
24
24
|
"params": {
|
|
25
|
-
"message": "Functions detected with high cyclomatic complexity (
|
|
25
|
+
"message": "Functions detected with high cyclomatic complexity (40+). Consider refactoring.",
|
|
26
26
|
"details": {
|
|
27
27
|
"fact": "complexityResult"
|
|
28
28
|
}
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
"resultFact": "functionCountResult"
|
|
15
15
|
},
|
|
16
16
|
"operator": "functionCount",
|
|
17
|
-
"value":
|
|
17
|
+
"value": 30
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
"event": {
|
|
22
22
|
"type": "warning",
|
|
23
23
|
"params": {
|
|
24
|
-
"message": "File contains too many functions (>
|
|
24
|
+
"message": "File contains too many functions (>30). Consider splitting into multiple files.",
|
|
25
25
|
"details": {
|
|
26
26
|
"fact": "functionCountResult"
|
|
27
27
|
}
|
package/dist/facts/index.test.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -112,8 +112,12 @@ function main() {
|
|
|
112
112
|
localConfigPath: cli_1.options.localConfigPath,
|
|
113
113
|
executionLogPrefix
|
|
114
114
|
});
|
|
115
|
-
|
|
116
|
-
const
|
|
115
|
+
logger_1.logger.info(`PERFORMANCE: Rule executions took ${resultMetadata.XFI_RESULT.durationSeconds} seconds`);
|
|
116
|
+
const reportGenerationStartTime = new Date().getTime();
|
|
117
|
+
let resultString = JSON.stringify(resultMetadata);
|
|
118
|
+
let prettyResult = prettyjson_1.default.render(resultMetadata.XFI_RESULT);
|
|
119
|
+
const reportGenerationdurationSeconds = ((new Date().getTime()) - reportGenerationStartTime) / 1000;
|
|
120
|
+
logger_1.logger.info(`PERFORMANCE: Report generation took ${reportGenerationdurationSeconds} seconds`);
|
|
117
121
|
// Add debug logging before notification check
|
|
118
122
|
logger_1.logger.debug({
|
|
119
123
|
notificationsEnabled: process.env.NOTIFICATIONS_ENABLED,
|
|
@@ -122,13 +126,28 @@ function main() {
|
|
|
122
126
|
}, 'Checking notification status');
|
|
123
127
|
// Send notifications if enabled
|
|
124
128
|
if (notificationConfig.enabled) {
|
|
129
|
+
const notificationStartTime = new Date().getTime();
|
|
125
130
|
logger_1.logger.debug('Notifications are enabled, preparing to send report');
|
|
126
131
|
logger_1.logger.debug({
|
|
127
132
|
affectedFilesCount: resultMetadata.XFI_RESULT.issueDetails.length
|
|
128
133
|
}, 'Preparing notification data');
|
|
129
134
|
// Pass the repo config to the notification manager
|
|
130
135
|
yield notificationManager.sendReport(resultMetadata);
|
|
136
|
+
const notificationDurationSeconds = ((new Date().getTime()) - notificationStartTime) / 1000;
|
|
137
|
+
logger_1.logger.info(`PERFORMANCE: Notifications took ${notificationDurationSeconds} seconds`);
|
|
131
138
|
}
|
|
139
|
+
// update overall duration and end time in XFI_RESULT
|
|
140
|
+
const endTime = new Date().getTime();
|
|
141
|
+
resultMetadata.XFI_RESULT.durationSeconds = (endTime - resultMetadata.XFI_RESULT.startTime) / 1000;
|
|
142
|
+
resultMetadata.XFI_RESULT.finishTime = endTime;
|
|
143
|
+
// change the finishTime value in the resultString to be endTimestamp
|
|
144
|
+
const resultStringWithEndTimestamp = resultString.replace(/("finishTime"):([\s]+)*([\d\.]+)/g, `$1:${endTime}`);
|
|
145
|
+
// change the durationSeconds value in the resultString to be the overall duration
|
|
146
|
+
resultString = resultStringWithEndTimestamp.replace(/("durationSeconds"):([\s]+)*([\d\.]+)/g, `$1:${resultMetadata.XFI_RESULT.durationSeconds}`);
|
|
147
|
+
// change the finishTime value in the prettyResult to be endTimestamp
|
|
148
|
+
const prettyResultWithEndTimestamp = prettyResult.replace(/(.*startTime.*34m)(\d*)(.*)/g, `$1$${endTime}$3`);
|
|
149
|
+
// change the durationSeconds value in the prettyResult to be the overall duration
|
|
150
|
+
prettyResult = prettyResultWithEndTimestamp.replace(/(.*durationSeconds.*34m)(\d*)(.*)/g, `$1${resultMetadata.XFI_RESULT.durationSeconds}$3`);
|
|
132
151
|
// if results are found, there were issues found in the codebase
|
|
133
152
|
if (resultMetadata.XFI_RESULT.totalIssues > 0) {
|
|
134
153
|
logger_1.logger.warn(`WARNING: lo-fi attributes detected in codebase. ${resultMetadata.XFI_RESULT.warningCount} are warnings, ${resultMetadata.XFI_RESULT.fatalityCount} are fatal.`);
|
|
@@ -250,7 +250,7 @@ class NotificationManager {
|
|
|
250
250
|
try {
|
|
251
251
|
// Failure template
|
|
252
252
|
fileDetails = results.XFI_RESULT.issueDetails.map(issue => {
|
|
253
|
-
logger_1.logger.
|
|
253
|
+
logger_1.logger.trace(issue, `generating report content for issue: ${issue.filePath}`);
|
|
254
254
|
// Remove local path prefix from file paths
|
|
255
255
|
const relativePath = issue.filePath.replace(results.XFI_RESULT.repoPath + '/', '');
|
|
256
256
|
const fileIssues = issue.errors;
|
|
@@ -280,7 +280,7 @@ class NotificationManager {
|
|
|
280
280
|
}).join('');
|
|
281
281
|
return output + errorDetails + '</ul></li>';
|
|
282
282
|
}).join('');
|
|
283
|
-
logger_1.logger.
|
|
283
|
+
logger_1.logger.trace(`fileDetails: ${output + ruleDetails}</ul></li>`);
|
|
284
284
|
return output + ruleDetails + '</ul></li>';
|
|
285
285
|
}).join('');
|
|
286
286
|
}
|
|
@@ -5,7 +5,10 @@ const fileContains_1 = require("./fileContains");
|
|
|
5
5
|
jest.mock('../utils/logger', () => ({
|
|
6
6
|
logger: {
|
|
7
7
|
debug: jest.fn(),
|
|
8
|
-
error: jest.fn()
|
|
8
|
+
error: jest.fn(),
|
|
9
|
+
trace: jest.fn(),
|
|
10
|
+
info: jest.fn(),
|
|
11
|
+
warn: jest.fn()
|
|
9
12
|
},
|
|
10
13
|
}));
|
|
11
14
|
describe('fileContains', () => {
|
|
@@ -6,7 +6,7 @@ const globalPatternCount = {
|
|
|
6
6
|
'name': 'globalPatternCount',
|
|
7
7
|
'fn': (analysisResult, threshold) => {
|
|
8
8
|
try {
|
|
9
|
-
logger_1.logger.
|
|
9
|
+
logger_1.logger.trace(`globalPatternCount: processing ${JSON.stringify(analysisResult)}`);
|
|
10
10
|
if (!analysisResult || !analysisResult.summary) {
|
|
11
11
|
logger_1.logger.debug('globalPatternCount: no analysis result available');
|
|
12
12
|
return false;
|
|
@@ -6,7 +6,7 @@ const globalPatternRatio = {
|
|
|
6
6
|
'name': 'globalPatternRatio',
|
|
7
7
|
'fn': (analysisResult, threshold) => {
|
|
8
8
|
try {
|
|
9
|
-
logger_1.logger.
|
|
9
|
+
logger_1.logger.trace(`globalPatternRatio: processing ${JSON.stringify(analysisResult)}`);
|
|
10
10
|
if (!analysisResult || !analysisResult.summary) {
|
|
11
11
|
logger_1.logger.debug('globalPatternRatio: no analysis result available');
|
|
12
12
|
return false;
|
|
@@ -5,7 +5,10 @@ const logger_1 = require("../utils/logger");
|
|
|
5
5
|
jest.mock('../utils/logger', () => ({
|
|
6
6
|
logger: {
|
|
7
7
|
debug: jest.fn(),
|
|
8
|
-
error: jest.fn()
|
|
8
|
+
error: jest.fn(),
|
|
9
|
+
trace: jest.fn(),
|
|
10
|
+
info: jest.fn(),
|
|
11
|
+
warn: jest.fn()
|
|
9
12
|
},
|
|
10
13
|
}));
|
|
11
14
|
describe('regexMatch', () => {
|
|
@@ -83,8 +83,18 @@ exports.functionComplexityFact = {
|
|
|
83
83
|
}))
|
|
84
84
|
}, 'Completed complexity analysis');
|
|
85
85
|
const result = {
|
|
86
|
-
complexities,
|
|
87
|
-
|
|
86
|
+
complexities: complexities.map(c => (Object.assign(Object.assign({}, c), { metrics: {
|
|
87
|
+
cyclomaticComplexity: c.complexity,
|
|
88
|
+
parameterCount: c.metrics.parameterCount,
|
|
89
|
+
returnCount: c.metrics.returnCount,
|
|
90
|
+
nestingDepth: c.metrics.nestingDepth,
|
|
91
|
+
cognitiveComplexity: c.metrics.cognitiveComplexity
|
|
92
|
+
} }))),
|
|
93
|
+
maxComplexity,
|
|
94
|
+
maxNestingDepth: Math.max(...complexities.map(c => c.metrics.nestingDepth)),
|
|
95
|
+
maxParameterCount: Math.max(...complexities.map(c => c.metrics.parameterCount)),
|
|
96
|
+
maxReturnCount: Math.max(...complexities.map(c => c.metrics.returnCount)),
|
|
97
|
+
maxCognitiveComplexity: Math.max(...complexities.map(c => c.metrics.cognitiveComplexity))
|
|
88
98
|
};
|
|
89
99
|
if (params === null || params === void 0 ? void 0 : params.resultFact) {
|
|
90
100
|
logger_1.logger.debug({ resultFact: params.resultFact }, 'Adding complexity results to almanac');
|
|
@@ -99,33 +109,62 @@ exports.functionComplexityFact = {
|
|
|
99
109
|
})
|
|
100
110
|
};
|
|
101
111
|
function analyzeFunctionComplexity(node) {
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
let cyclomaticComplexity = 1;
|
|
113
|
+
let parameterCount = 0;
|
|
114
|
+
let returnCount = 0;
|
|
115
|
+
let maxNestingDepth = 0;
|
|
116
|
+
let cognitiveComplexity = 0;
|
|
117
|
+
let currentNestingDepth = 0;
|
|
118
|
+
// Count parameters
|
|
119
|
+
const parameterList = node.descendantsOfType('formal_parameters')[0];
|
|
120
|
+
if (parameterList) {
|
|
121
|
+
parameterCount = parameterList.namedChildCount;
|
|
122
|
+
}
|
|
123
|
+
function visit(node, depth = 0) {
|
|
124
|
+
currentNestingDepth = depth;
|
|
125
|
+
maxNestingDepth = Math.max(maxNestingDepth, depth);
|
|
109
126
|
switch (node.type) {
|
|
110
127
|
case 'if_statement':
|
|
111
128
|
case 'switch_case':
|
|
129
|
+
cyclomaticComplexity++;
|
|
130
|
+
cognitiveComplexity += depth + 1;
|
|
131
|
+
break;
|
|
112
132
|
case 'for_statement':
|
|
113
133
|
case 'while_statement':
|
|
114
134
|
case 'do_statement':
|
|
135
|
+
cyclomaticComplexity++;
|
|
136
|
+
cognitiveComplexity += depth + 2; // Loops are more complex
|
|
137
|
+
break;
|
|
115
138
|
case 'try_statement':
|
|
116
|
-
|
|
139
|
+
cyclomaticComplexity++;
|
|
140
|
+
cognitiveComplexity += 1;
|
|
117
141
|
break;
|
|
118
142
|
case '&&':
|
|
119
143
|
case '||':
|
|
120
|
-
|
|
144
|
+
cyclomaticComplexity += 0.5;
|
|
145
|
+
cognitiveComplexity += 1;
|
|
146
|
+
break;
|
|
147
|
+
case 'return_statement':
|
|
148
|
+
returnCount++;
|
|
121
149
|
break;
|
|
122
150
|
}
|
|
151
|
+
// Visit children with increased depth for control structures
|
|
152
|
+
const increaseDepthFor = [
|
|
153
|
+
'if_statement', 'for_statement', 'while_statement',
|
|
154
|
+
'do_statement', 'try_statement', 'switch_case'
|
|
155
|
+
];
|
|
123
156
|
for (let child of node.children || []) {
|
|
124
|
-
visit(child);
|
|
157
|
+
visit(child, increaseDepthFor.includes(node.type) ? depth + 1 : depth);
|
|
125
158
|
}
|
|
126
159
|
}
|
|
127
160
|
visit(node);
|
|
128
|
-
return
|
|
161
|
+
return {
|
|
162
|
+
cyclomaticComplexity,
|
|
163
|
+
parameterCount,
|
|
164
|
+
returnCount,
|
|
165
|
+
nestingDepth: maxNestingDepth,
|
|
166
|
+
cognitiveComplexity
|
|
167
|
+
};
|
|
129
168
|
}
|
|
130
169
|
function getFunctionName(node) {
|
|
131
170
|
if (node.type === 'function_declaration') {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.effectCleanupFact = void 0;
|
|
13
|
+
const logger_1 = require("../../../utils/logger");
|
|
14
|
+
const astUtils_1 = require("../../../utils/astUtils");
|
|
15
|
+
exports.effectCleanupFact = {
|
|
16
|
+
name: 'effectCleanup',
|
|
17
|
+
fn: (params, almanac) => __awaiter(void 0, void 0, void 0, function* () {
|
|
18
|
+
try {
|
|
19
|
+
const fileData = yield almanac.factValue('fileData');
|
|
20
|
+
const { tree } = (0, astUtils_1.generateAst)(fileData);
|
|
21
|
+
if (!tree)
|
|
22
|
+
return { issues: [] };
|
|
23
|
+
const issues = [];
|
|
24
|
+
function visit(node) {
|
|
25
|
+
var _a, _b, _c, _d, _e;
|
|
26
|
+
if (node.type === 'call_expression' &&
|
|
27
|
+
((_b = (_a = node.children) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.type) === 'identifier' &&
|
|
28
|
+
node.children[0].text === 'useEffect') {
|
|
29
|
+
const effectBody = (_e = (_d = (_c = node.children) === null || _c === void 0 ? void 0 : _c[1]) === null || _d === void 0 ? void 0 : _d.children) === null || _e === void 0 ? void 0 : _e[0];
|
|
30
|
+
if (!effectBody)
|
|
31
|
+
return;
|
|
32
|
+
// Check if effect returns a cleanup function
|
|
33
|
+
const hasCleanup = hasReturnFunction(effectBody);
|
|
34
|
+
// Look for patterns that typically need cleanup
|
|
35
|
+
const needsCleanup = checkNeedsCleanup(effectBody);
|
|
36
|
+
if (needsCleanup && !hasCleanup) {
|
|
37
|
+
issues.push({
|
|
38
|
+
type: 'missingCleanup',
|
|
39
|
+
line: node.startPosition.row + 1,
|
|
40
|
+
message: 'useEffect may need cleanup for subscriptions/listeners/timers'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const child of node.children || []) {
|
|
45
|
+
visit(child);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
visit(tree.rootNode);
|
|
49
|
+
const result = {
|
|
50
|
+
issues,
|
|
51
|
+
fileInfo: {
|
|
52
|
+
path: fileData.filePath,
|
|
53
|
+
issueCount: issues.length
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
if (params === null || params === void 0 ? void 0 : params.resultFact) {
|
|
57
|
+
almanac.addRuntimeFact(params.resultFact, result);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger_1.logger.error(`Error in effectCleanup fact: ${error}`);
|
|
63
|
+
return { issues: [] };
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
};
|
|
67
|
+
function hasReturnFunction(node) {
|
|
68
|
+
if (node.type === 'return_statement') {
|
|
69
|
+
const returnValue = node.children[0];
|
|
70
|
+
return (returnValue === null || returnValue === void 0 ? void 0 : returnValue.type) === 'function' ||
|
|
71
|
+
(returnValue === null || returnValue === void 0 ? void 0 : returnValue.type) === 'arrow_function';
|
|
72
|
+
}
|
|
73
|
+
for (const child of node.children || []) {
|
|
74
|
+
if (hasReturnFunction(child))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
function checkNeedsCleanup(node) {
|
|
80
|
+
const cleanupPatterns = [
|
|
81
|
+
'addEventListener',
|
|
82
|
+
'setTimeout',
|
|
83
|
+
'setInterval',
|
|
84
|
+
'subscribe',
|
|
85
|
+
'observe'
|
|
86
|
+
];
|
|
87
|
+
if (node.type === 'call_expression') {
|
|
88
|
+
const callee = node.children[0];
|
|
89
|
+
if ((callee === null || callee === void 0 ? void 0 : callee.type) === 'identifier' &&
|
|
90
|
+
cleanupPatterns.some(p => callee.text.includes(p))) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const child of node.children || []) {
|
|
95
|
+
if (checkNeedsCleanup(child))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.hookDependencyFact = void 0;
|
|
13
|
+
const logger_1 = require("../../../utils/logger");
|
|
14
|
+
const astUtils_1 = require("../../../utils/astUtils");
|
|
15
|
+
exports.hookDependencyFact = {
|
|
16
|
+
name: 'hookDependency',
|
|
17
|
+
fn: (params, almanac) => __awaiter(void 0, void 0, void 0, function* () {
|
|
18
|
+
try {
|
|
19
|
+
const fileData = yield almanac.factValue('fileData');
|
|
20
|
+
const { tree } = (0, astUtils_1.generateAst)(fileData);
|
|
21
|
+
if (!tree)
|
|
22
|
+
return { issues: [] };
|
|
23
|
+
const issues = [];
|
|
24
|
+
// Find all useEffect calls
|
|
25
|
+
function visit(node) {
|
|
26
|
+
var _a, _b, _c, _d;
|
|
27
|
+
if (node.type === 'call_expression' &&
|
|
28
|
+
((_b = (_a = node.children) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.type) === 'identifier' &&
|
|
29
|
+
node.children[0].text === 'useEffect') {
|
|
30
|
+
// Get the dependency array argument
|
|
31
|
+
const args = node.children.filter((n) => n.type === 'arguments')[0];
|
|
32
|
+
const depArray = (_c = args === null || args === void 0 ? void 0 : args.children) === null || _c === void 0 ? void 0 : _c[1]; // Second argument should be dep array
|
|
33
|
+
// Check for missing dependency array
|
|
34
|
+
if (!depArray) {
|
|
35
|
+
issues.push({
|
|
36
|
+
type: 'missingDeps',
|
|
37
|
+
line: node.startPosition.row + 1,
|
|
38
|
+
message: 'useEffect missing dependency array'
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Check for empty dependency array when effect uses external values
|
|
43
|
+
const effectBody = args.children[0];
|
|
44
|
+
const externalRefs = findExternalReferences(effectBody);
|
|
45
|
+
const deps = ((_d = depArray.children) === null || _d === void 0 ? void 0 : _d.map((n) => n.text)) || [];
|
|
46
|
+
const missingDeps = externalRefs.filter(ref => !deps.includes(ref));
|
|
47
|
+
if (missingDeps.length > 0) {
|
|
48
|
+
issues.push({
|
|
49
|
+
type: 'incompleteDeps',
|
|
50
|
+
line: node.startPosition.row + 1,
|
|
51
|
+
message: `useEffect has missing dependencies: ${missingDeps.join(', ')}`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const child of node.children || []) {
|
|
56
|
+
visit(child);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
visit(tree.rootNode);
|
|
60
|
+
const result = {
|
|
61
|
+
issues,
|
|
62
|
+
fileInfo: {
|
|
63
|
+
path: fileData.filePath,
|
|
64
|
+
issueCount: issues.length
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
if (params === null || params === void 0 ? void 0 : params.resultFact) {
|
|
68
|
+
almanac.addRuntimeFact(params.resultFact, result);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger_1.logger.error(`Error in hookDependency fact: ${error}`);
|
|
74
|
+
return { issues: [] };
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
function findExternalReferences(node) {
|
|
79
|
+
const refs = new Set();
|
|
80
|
+
function visit(node, scope) {
|
|
81
|
+
if (node.type === 'identifier') {
|
|
82
|
+
if (!scope.has(node.text)) {
|
|
83
|
+
refs.add(node.text);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (node.type === 'variable_declaration') {
|
|
87
|
+
// Add declared variables to scope
|
|
88
|
+
const declarators = node.children.filter((n) => n.type === 'variable_declarator');
|
|
89
|
+
declarators.forEach((d) => {
|
|
90
|
+
var _a;
|
|
91
|
+
const name = (_a = d.children[0]) === null || _a === void 0 ? void 0 : _a.text;
|
|
92
|
+
if (name)
|
|
93
|
+
scope.add(name);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const child of node.children || []) {
|
|
97
|
+
visit(child, scope);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
visit(node, new Set());
|
|
101
|
+
return Array.from(refs);
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { plugin } from './xfiPluginReactPatterns';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.plugin = void 0;
|
|
4
|
+
var xfiPluginReactPatterns_1 = require("./xfiPluginReactPatterns");
|
|
5
|
+
Object.defineProperty(exports, "plugin", { enumerable: true, get: function () { return xfiPluginReactPatterns_1.plugin; } });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.plugin = void 0;
|
|
4
|
+
const hookDependencyFact_1 = require("./facts/hookDependencyFact");
|
|
5
|
+
const effectCleanupFact_1 = require("./facts/effectCleanupFact");
|
|
6
|
+
const plugin = {
|
|
7
|
+
name: 'xfiPluginReactPatterns',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
facts: [hookDependencyFact_1.hookDependencyFact, effectCleanupFact_1.effectCleanupFact],
|
|
10
|
+
onError: (error) => ({
|
|
11
|
+
message: `React patterns analysis error: ${error.message}`,
|
|
12
|
+
level: 'warning',
|
|
13
|
+
details: error.stack
|
|
14
|
+
})
|
|
15
|
+
};
|
|
16
|
+
exports.plugin = plugin;
|