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.
Files changed (76) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/core/configManager.test.js +2 -1
  3. package/dist/core/engine/engineRunner.js +1 -1
  4. package/dist/core/engine/engineRunner.test.js +1 -0
  5. package/dist/core/pluginRegistry.test.js +1 -0
  6. package/dist/demoConfig/node-fullstack-exemptions/project1-node-fullstack-exemptions.json +2 -2
  7. package/dist/demoConfig/rules/functionComplexity-iterative-rule.json +3 -3
  8. package/dist/demoConfig/rules/functionCount-iterative-rule.json +2 -2
  9. package/dist/facts/globalFileAnalysisFacts.test.js +1 -0
  10. package/dist/facts/index.test.js +3 -1
  11. package/dist/facts/openaiAnalysisFacts.test.js +1 -0
  12. package/dist/index.js +21 -2
  13. package/dist/notifications/notificationManager.js +2 -2
  14. package/dist/operators/fileContains.test.js +4 -1
  15. package/dist/operators/globalPatternCount.js +1 -1
  16. package/dist/operators/globalPatternCount.test.js +2 -0
  17. package/dist/operators/globalPatternRatio.js +1 -1
  18. package/dist/operators/globalPatternRatio.test.js +2 -0
  19. package/dist/operators/openaiAnalysisHighSeverity.test.js +2 -0
  20. package/dist/operators/regexMatch.test.js +4 -1
  21. package/dist/plugins/xfiPluginAst/facts/functionComplexityFact.js +52 -13
  22. package/dist/plugins/xfiPluginReactPatterns/facts/effectCleanupFact.d.ts +2 -0
  23. package/dist/plugins/xfiPluginReactPatterns/facts/effectCleanupFact.js +99 -0
  24. package/dist/plugins/xfiPluginReactPatterns/facts/hookDependencyFact.d.ts +2 -0
  25. package/dist/plugins/xfiPluginReactPatterns/facts/hookDependencyFact.js +102 -0
  26. package/dist/plugins/xfiPluginReactPatterns/index.d.ts +1 -0
  27. package/dist/plugins/xfiPluginReactPatterns/index.js +5 -0
  28. package/dist/plugins/xfiPluginReactPatterns/xfiPluginReactPatterns.d.ts +3 -0
  29. package/dist/plugins/xfiPluginReactPatterns/xfiPluginReactPatterns.js +16 -0
  30. package/dist/plugins/xfiPluginRemoteStringValidator/facts/remoteSubstringValidation.test.js +2 -0
  31. package/dist/plugins/xfiPluginRemoteStringValidator/operators/invalidRemoteValidation.test.js +2 -0
  32. package/dist/plugins/xfiPluginRequiredFiles/operators/missingRequiredFiles.test.js +4 -1
  33. package/dist/server/cacheManager.test.js +2 -1
  34. package/dist/utils/exemptionUtils.js +15 -4
  35. package/dist/utils/exemptionUtils.test.js +1 -0
  36. package/dist/utils/ruleUtils.test.js +1 -0
  37. package/dist/utils/telemetry.js +1 -1
  38. package/dist/xfidelity +21 -2
  39. package/package.json +4 -6
  40. package/src/IDEAS.md +294 -84
  41. package/src/core/configManager.test.ts +2 -1
  42. package/src/core/engine/engineRunner.test.ts +1 -0
  43. package/src/core/engine/engineRunner.ts +1 -1
  44. package/src/core/pluginRegistry.test.ts +1 -0
  45. package/src/demoConfig/node-fullstack-exemptions/project1-node-fullstack-exemptions.json +2 -2
  46. package/src/demoConfig/rules/functionComplexity-iterative-rule.json +3 -3
  47. package/src/demoConfig/rules/functionCount-iterative-rule.json +2 -2
  48. package/src/exampleTriggerFiles/mixedUIComponents.tsx +9 -9
  49. package/src/facts/globalFileAnalysisFacts.test.ts +1 -0
  50. package/src/facts/index.test.ts +3 -1
  51. package/src/facts/openaiAnalysisFacts.test.ts +1 -0
  52. package/src/index.ts +31 -2
  53. package/src/notifications/notificationManager.ts +2 -2
  54. package/src/operators/fileContains.test.ts +4 -1
  55. package/src/operators/globalPatternCount.test.ts +2 -0
  56. package/src/operators/globalPatternCount.ts +1 -1
  57. package/src/operators/globalPatternRatio.test.ts +2 -0
  58. package/src/operators/globalPatternRatio.ts +1 -1
  59. package/src/operators/openaiAnalysisHighSeverity.test.ts +2 -0
  60. package/src/operators/regexMatch.test.ts +4 -1
  61. package/src/plugins/xfiPluginAst/facts/functionComplexityFact.ts +67 -14
  62. package/src/plugins/xfiPluginReactPatterns/facts/effectCleanupFact.ts +103 -0
  63. package/src/plugins/xfiPluginReactPatterns/facts/hookDependencyFact.ts +103 -0
  64. package/src/plugins/xfiPluginReactPatterns/index.ts +1 -0
  65. package/src/plugins/xfiPluginReactPatterns/sampleRules/effectCleanup-iterative-rule.json +30 -0
  66. package/src/plugins/xfiPluginReactPatterns/sampleRules/hookDependency-iterative-rule.json +30 -0
  67. package/src/plugins/xfiPluginReactPatterns/xfiPluginReactPatterns.ts +16 -0
  68. package/src/plugins/xfiPluginRemoteStringValidator/facts/remoteSubstringValidation.test.ts +2 -0
  69. package/src/plugins/xfiPluginRemoteStringValidator/operators/invalidRemoteValidation.test.ts +2 -0
  70. package/src/plugins/xfiPluginRemoteStringValidator/sampleRules/xfiTestMatch.json +0 -6
  71. package/src/plugins/xfiPluginRequiredFiles/operators/missingRequiredFiles.test.ts +4 -1
  72. package/src/server/cacheManager.test.ts +2 -1
  73. package/src/utils/exemptionUtils.test.ts +1 -0
  74. package/src/utils/exemptionUtils.ts +23 -5
  75. package/src/utils/ruleUtils.test.ts +1 -0
  76. 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
 
@@ -118,7 +118,8 @@ jest.mock('../utils/logger', () => ({
118
118
  debug: jest.fn(),
119
119
  error: jest.fn(),
120
120
  info: jest.fn(),
121
- warn: jest.fn()
121
+ warn: jest.fn(),
122
+ trace: jest.fn()
122
123
  },
123
124
  setLogPrefix: jest.fn()
124
125
  }));
@@ -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.debug(JSON.stringify(result));
46
+ logger_1.logger.trace(JSON.stringify(result));
47
47
  if (result.result) {
48
48
  fileFailures.push({
49
49
  ruleFailure: result.name,
@@ -19,6 +19,7 @@ jest.mock('../../utils/logger', () => ({
19
19
  debug: jest.fn(),
20
20
  error: jest.fn(),
21
21
  warn: jest.fn(),
22
+ trace: jest.fn()
22
23
  },
23
24
  }));
24
25
  describe('runEngineOnFiles', () => {
@@ -8,6 +8,7 @@ jest.mock('../utils/logger', () => ({
8
8
  warn: jest.fn(),
9
9
  error: jest.fn(),
10
10
  debug: jest.fn(),
11
+ trace: jest.fn()
11
12
  },
12
13
  }));
13
14
  describe('XFiPluginRegistry', () => {
@@ -1,8 +1,8 @@
1
1
  [
2
2
  {
3
3
  "repoUrl": "git@github.com:zotoio/x-fidelity.git",
4
- "rule": "outdatedFramework-global",
5
- "expirationDate": "2023-12-31",
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": 8
15
+ "minimumComplexityLogged": 25
16
16
  },
17
17
  "operator": "astComplexity",
18
- "value": 10
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 (10+). Consider refactoring.",
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": 20
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 (>20). Consider splitting into multiple files.",
24
+ "message": "File contains too many functions (>30). Consider splitting into multiple files.",
25
25
  "details": {
26
26
  "fact": "functionCountResult"
27
27
  }
@@ -17,6 +17,7 @@ jest.mock('../utils/logger', () => ({
17
17
  error: jest.fn(),
18
18
  info: jest.fn(),
19
19
  warn: jest.fn(),
20
+ trace: jest.fn()
20
21
  },
21
22
  }));
22
23
  describe('globalFileAnalysis', () => {
@@ -27,7 +27,9 @@ jest.mock('../utils/logger', () => ({
27
27
  logger: {
28
28
  info: jest.fn(),
29
29
  debug: jest.fn(),
30
- warn: jest.fn()
30
+ warn: jest.fn(),
31
+ trace: jest.fn(),
32
+ error: jest.fn()
31
33
  }
32
34
  }));
33
35
  describe('loadFacts', () => {
@@ -22,6 +22,7 @@ jest.mock('../utils/logger', () => ({
22
22
  error: jest.fn(),
23
23
  info: jest.fn(),
24
24
  warn: jest.fn(),
25
+ trace: jest.fn()
25
26
  },
26
27
  }));
27
28
  jest.mock('openai', () => {
package/dist/index.js CHANGED
@@ -112,8 +112,12 @@ function main() {
112
112
  localConfigPath: cli_1.options.localConfigPath,
113
113
  executionLogPrefix
114
114
  });
115
- const resultString = JSON.stringify(resultMetadata);
116
- const prettyResult = prettyjson_1.default.render(resultMetadata.XFI_RESULT);
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.debug(issue, `generating report content for issue: ${issue.filePath}`);
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.debug(`fileDetails: ${output + ruleDetails}</ul></li>`);
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.debug(`globalPatternCount: processing ${JSON.stringify(analysisResult)}`);
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,6 +6,8 @@ jest.mock('../utils/logger', () => ({
6
6
  debug: jest.fn(),
7
7
  error: jest.fn(),
8
8
  info: jest.fn(),
9
+ trace: jest.fn(),
10
+ warn: jest.fn()
9
11
  },
10
12
  }));
11
13
  describe('globalPatternCount', () => {
@@ -6,7 +6,7 @@ const globalPatternRatio = {
6
6
  'name': 'globalPatternRatio',
7
7
  'fn': (analysisResult, threshold) => {
8
8
  try {
9
- logger_1.logger.debug(`globalPatternRatio: processing ${JSON.stringify(analysisResult)}`);
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;
@@ -6,6 +6,8 @@ jest.mock('../utils/logger', () => ({
6
6
  debug: jest.fn(),
7
7
  error: jest.fn(),
8
8
  info: jest.fn(),
9
+ trace: jest.fn(),
10
+ warn: jest.fn()
9
11
  },
10
12
  }));
11
13
  describe('globalPatternRatio', () => {
@@ -7,6 +7,8 @@ jest.mock('../utils/logger', () => ({
7
7
  error: jest.fn(),
8
8
  debug: jest.fn(),
9
9
  info: jest.fn(),
10
+ trace: jest.fn(),
11
+ warn: jest.fn()
10
12
  },
11
13
  }));
12
14
  describe('openaiAnalysisHighSeverity', () => {
@@ -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
- maxComplexity
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 complexity = 1; // Base complexity
103
- // Walk the AST and count:
104
- // - Conditional statements (if, else, switch cases)
105
- // - Loops (for, while, do-while)
106
- // - Logical operators (&&, ||)
107
- // - Try/catch blocks
108
- function visit(node) {
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
- complexity++;
139
+ cyclomaticComplexity++;
140
+ cognitiveComplexity += 1;
117
141
  break;
118
142
  case '&&':
119
143
  case '||':
120
- complexity += 0.5;
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 complexity;
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,2 @@
1
+ import { FactDefn } from '../../../types/typeDefs';
2
+ export declare const effectCleanupFact: FactDefn;
@@ -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,2 @@
1
+ import { FactDefn } from '../../../types/typeDefs';
2
+ export declare const hookDependencyFact: FactDefn;
@@ -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,3 @@
1
+ import { XFiPlugin } from '../../types/typeDefs';
2
+ declare const plugin: XFiPlugin;
3
+ export { 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;
@@ -16,6 +16,8 @@ jest.mock('../../../utils/logger', () => ({
16
16
  debug: jest.fn(),
17
17
  error: jest.fn(),
18
18
  info: jest.fn(),
19
+ trace: jest.fn(),
20
+ warn: jest.fn()
19
21
  },
20
22
  }));
21
23
  describe('remoteSubstringValidation', () => {
@@ -6,6 +6,8 @@ jest.mock('../../../utils/logger', () => ({
6
6
  debug: jest.fn(),
7
7
  error: jest.fn(),
8
8
  info: jest.fn(),
9
+ trace: jest.fn(),
10
+ warn: jest.fn()
9
11
  },
10
12
  }));
11
13
  describe('invalidRemoteValidation', () => {