react-code-smell-detector 1.3.1 → 1.4.2

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 (39) hide show
  1. package/README.md +294 -4
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +29 -1
  4. package/dist/baseline.d.ts +37 -0
  5. package/dist/baseline.d.ts.map +1 -0
  6. package/dist/baseline.js +112 -0
  7. package/dist/bundleAnalyzer.d.ts +25 -0
  8. package/dist/bundleAnalyzer.d.ts.map +1 -0
  9. package/dist/bundleAnalyzer.js +375 -0
  10. package/dist/cli.js +74 -0
  11. package/dist/customRules.d.ts +31 -0
  12. package/dist/customRules.d.ts.map +1 -0
  13. package/dist/customRules.js +289 -0
  14. package/dist/detectors/complexity.d.ts +0 -4
  15. package/dist/detectors/complexity.d.ts.map +1 -1
  16. package/dist/detectors/complexity.js +1 -1
  17. package/dist/detectors/deadCode.d.ts +0 -7
  18. package/dist/detectors/deadCode.d.ts.map +1 -1
  19. package/dist/detectors/deadCode.js +0 -24
  20. package/dist/detectors/index.d.ts +3 -2
  21. package/dist/detectors/index.d.ts.map +1 -1
  22. package/dist/detectors/index.js +3 -2
  23. package/dist/detectors/unusedCode.d.ts +7 -0
  24. package/dist/detectors/unusedCode.d.ts.map +1 -0
  25. package/dist/detectors/unusedCode.js +78 -0
  26. package/dist/git.d.ts +3 -0
  27. package/dist/git.d.ts.map +1 -1
  28. package/dist/git.js +13 -0
  29. package/dist/graphGenerator.d.ts +34 -0
  30. package/dist/graphGenerator.d.ts.map +1 -0
  31. package/dist/graphGenerator.js +320 -0
  32. package/dist/reporter.js +5 -0
  33. package/dist/types/index.d.ts +12 -1
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/types/index.js +17 -0
  36. package/dist/webhooks.d.ts +20 -0
  37. package/dist/webhooks.d.ts.map +1 -0
  38. package/dist/webhooks.js +199 -0
  39. package/package.json +3 -1
@@ -0,0 +1,375 @@
1
+ import _traverse from '@babel/traverse';
2
+ const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
3
+ /**
4
+ * Estimate bundle size impact per component
5
+ */
6
+ export function analyzeBundleImpact(components, files, sourceCode) {
7
+ const componentInfo = [];
8
+ for (const component of components) {
9
+ const info = estimateComponentSize(component, sourceCode);
10
+ componentInfo.push(info);
11
+ }
12
+ // Sort by size
13
+ componentInfo.sort((a, b) => b.estimatedSize - a.estimatedSize);
14
+ const totalSize = componentInfo.reduce((sum, c) => sum + c.estimatedSize, 0);
15
+ const avgSize = totalSize / componentInfo.length || 0;
16
+ const recommendations = generateBundleRecommendations(componentInfo, totalSize);
17
+ return {
18
+ components: componentInfo,
19
+ totalEstimatedSize: totalSize,
20
+ averageComponentSize: avgSize,
21
+ largestComponents: componentInfo.slice(0, 5),
22
+ recommendations,
23
+ };
24
+ }
25
+ /**
26
+ * Estimate size of a single component in bytes
27
+ */
28
+ function estimateComponentSize(component, sourceCode) {
29
+ let size = 0;
30
+ let lineCount = 0;
31
+ let exportCount = 0;
32
+ let dependencyCount = 0;
33
+ let complexity = 0;
34
+ // Count lines
35
+ if (component.code) {
36
+ lineCount = component.code.split('\n').length;
37
+ size += lineCount * 4; // ~4 bytes per line average
38
+ }
39
+ // Count exports
40
+ component.path.traverse({
41
+ ExportNamedDeclaration() {
42
+ exportCount++;
43
+ size += 100; // export overhead
44
+ },
45
+ ExportDefaultDeclaration() {
46
+ exportCount++;
47
+ size += 100;
48
+ },
49
+ });
50
+ // Count dependencies
51
+ component.path.traverse({
52
+ ImportDeclaration() {
53
+ dependencyCount++;
54
+ size += 80;
55
+ },
56
+ CallExpression(path) {
57
+ if (path.node.callee.type === 'Import') {
58
+ dependencyCount++;
59
+ size += 100; // dynamic import is heavier
60
+ }
61
+ },
62
+ });
63
+ // Estimate complexity
64
+ component.path.traverse({
65
+ IfStatement() {
66
+ complexity++;
67
+ size += 30;
68
+ },
69
+ FunctionDeclaration() {
70
+ complexity++;
71
+ size += 50;
72
+ },
73
+ ArrowFunctionExpression() {
74
+ complexity++;
75
+ size += 40;
76
+ },
77
+ });
78
+ // Add base overhead
79
+ size += 200;
80
+ const impact = determineImpact(size, lineCount, complexity);
81
+ return {
82
+ name: component.name || 'Anonymous',
83
+ file: component.file || 'unknown',
84
+ estimatedSize: size,
85
+ dependencies: dependencyCount,
86
+ exports: exportCount,
87
+ complexity: lineCount,
88
+ impact,
89
+ };
90
+ }
91
+ /**
92
+ * Determine bundle impact level
93
+ */
94
+ function determineImpact(size, lines, complexity) {
95
+ let score = 0;
96
+ // Size factor
97
+ if (size > 10000)
98
+ score += 3; // 10KB+
99
+ else if (size > 5000)
100
+ score += 2; // 5KB+
101
+ else if (size > 2000)
102
+ score += 1; // 2KB+
103
+ // Lines factor
104
+ if (lines > 500)
105
+ score += 3;
106
+ else if (lines > 300)
107
+ score += 2;
108
+ else if (lines > 150)
109
+ score += 1;
110
+ // Complexity factor
111
+ if (complexity > 50)
112
+ score += 2;
113
+ else if (complexity > 30)
114
+ score += 1;
115
+ if (score >= 7)
116
+ return 'critical';
117
+ if (score >= 5)
118
+ return 'high';
119
+ if (score >= 2)
120
+ return 'medium';
121
+ return 'low';
122
+ }
123
+ /**
124
+ * Generate recommendations to reduce bundle size
125
+ */
126
+ function generateBundleRecommendations(components, totalSize) {
127
+ const recommendations = [];
128
+ // Check for large components
129
+ const largeComps = components.filter(c => c.impact === 'critical' || c.impact === 'high');
130
+ if (largeComps.length > 0) {
131
+ recommendations.push(`Consider splitting large components: ${largeComps.slice(0, 3).map(c => c.name).join(', ')}`);
132
+ }
133
+ // Check for high dependency count
134
+ const highDepComps = components.filter(c => c.dependencies > 10);
135
+ if (highDepComps.length > 0) {
136
+ recommendations.push(`These components have many dependencies (${highDepComps.length}). Consider lazy loading or code splitting.`);
137
+ }
138
+ // Check total size
139
+ if (totalSize > 100000) {
140
+ recommendations.push(`Total estimated bundle size is ${(totalSize / 1024).toFixed(1)}KB. Consider code-splitting or tree-shaking unused imports.`);
141
+ }
142
+ // Check for high complexity
143
+ const complexComps = components.filter(c => c.complexity > 300);
144
+ if (complexComps.length > 0) {
145
+ recommendations.push(`${complexComps.length} component(s) have high complexity (>300 LOC). Extract logic to custom hooks or utilities.`);
146
+ }
147
+ return recommendations.length > 0
148
+ ? recommendations
149
+ : ['✅ Bundle size is well-optimized. Keep monitoring as code grows.'];
150
+ }
151
+ /**
152
+ * Generate HTML bundle analysis report
153
+ */
154
+ export function generateBundleReport(analysis, projectName) {
155
+ const formatSize = (bytes) => {
156
+ if (bytes > 1024 * 1024)
157
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
158
+ if (bytes > 1024)
159
+ return `${(bytes / 1024).toFixed(1)}KB`;
160
+ return `${bytes}B`;
161
+ };
162
+ const impactColor = {
163
+ low: '#4caf50',
164
+ medium: '#ff9800',
165
+ high: '#f44336',
166
+ critical: '#b71c1c',
167
+ };
168
+ return `<!DOCTYPE html>
169
+ <html lang="en">
170
+ <head>
171
+ <meta charset="UTF-8">
172
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
173
+ <title>Bundle Analysis - ${projectName}</title>
174
+ <style>
175
+ body {
176
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
177
+ background: #f5f5f5;
178
+ margin: 0;
179
+ padding: 20px;
180
+ }
181
+ .container {
182
+ max-width: 1200px;
183
+ margin: 0 auto;
184
+ background: white;
185
+ border-radius: 8px;
186
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
187
+ padding: 30px;
188
+ }
189
+ h1 {
190
+ color: #1976d2;
191
+ margin: 0 0 10px 0;
192
+ }
193
+ .stats-grid {
194
+ display: grid;
195
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
196
+ gap: 20px;
197
+ margin: 30px 0;
198
+ }
199
+ .stat-card {
200
+ background: #f8f9fa;
201
+ border-left: 4px solid #1976d2;
202
+ padding: 20px;
203
+ border-radius: 4px;
204
+ }
205
+ .stat-label {
206
+ font-size: 12px;
207
+ color: #666;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.5px;
210
+ }
211
+ .stat-value {
212
+ font-size: 28px;
213
+ font-weight: bold;
214
+ color: #1976d2;
215
+ margin-top: 8px;
216
+ }
217
+ .recommendations {
218
+ background: #e3f2fd;
219
+ border-left: 4px solid #1976d2;
220
+ padding: 20px;
221
+ border-radius: 4px;
222
+ margin: 30px 0;
223
+ }
224
+ .recommendations h3 {
225
+ margin: 0 0 10px 0;
226
+ color: #1976d2;
227
+ }
228
+ .recommendations ul {
229
+ margin: 0;
230
+ padding-left: 20px;
231
+ }
232
+ .recommendations li {
233
+ margin: 8px 0;
234
+ color: #333;
235
+ }
236
+ table {
237
+ width: 100%;
238
+ border-collapse: collapse;
239
+ margin-top: 30px;
240
+ }
241
+ th {
242
+ background: #f5f5f5;
243
+ padding: 12px;
244
+ text-align: left;
245
+ font-weight: 600;
246
+ border-bottom: 2px solid #e0e0e0;
247
+ }
248
+ td {
249
+ padding: 12px;
250
+ border-bottom: 1px solid #e0e0e0;
251
+ }
252
+ tr:hover {
253
+ background: #fafafa;
254
+ }
255
+ .impact-badge {
256
+ display: inline-block;
257
+ padding: 4px 12px;
258
+ border-radius: 12px;
259
+ font-size: 12px;
260
+ font-weight: 600;
261
+ color: white;
262
+ }
263
+ .impact-low {
264
+ background: #4caf50;
265
+ }
266
+ .impact-medium {
267
+ background: #ff9800;
268
+ }
269
+ .impact-high {
270
+ background: #f44336;
271
+ }
272
+ .impact-critical {
273
+ background: #b71c1c;
274
+ }
275
+ .size-bar {
276
+ display: inline-block;
277
+ height: 8px;
278
+ background: #1976d2;
279
+ border-radius: 4px;
280
+ margin-right: 8px;
281
+ vertical-align: middle;
282
+ }
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <div class="container">
287
+ <h1>📦 Bundle Size Analysis - ${projectName}</h1>
288
+ <p style="color: #666; margin-top: 5px;">Estimated size impact per component</p>
289
+
290
+ <div class="stats-grid">
291
+ <div class="stat-card">
292
+ <div class="stat-label">Total Size</div>
293
+ <div class="stat-value">${formatSize(analysis.totalEstimatedSize)}</div>
294
+ </div>
295
+ <div class="stat-card">
296
+ <div class="stat-label">Components</div>
297
+ <div class="stat-value">${analysis.components.length}</div>
298
+ </div>
299
+ <div class="stat-card">
300
+ <div class="stat-label">Avg Component</div>
301
+ <div class="stat-value">${formatSize(analysis.averageComponentSize)}</div>
302
+ </div>
303
+ </div>
304
+
305
+ ${analysis.recommendations.length > 0
306
+ ? `
307
+ <div class="recommendations">
308
+ <h3>💡 Recommendations</h3>
309
+ <ul>
310
+ ${analysis.recommendations.map(rec => `<li>${rec}</li>`).join('')}
311
+ </ul>
312
+ </div>
313
+ `
314
+ : ''}
315
+
316
+ <h2 style="margin-top: 40px; color: #333;">📊 Component Breakdown</h2>
317
+ <table>
318
+ <thead>
319
+ <tr>
320
+ <th>Component</th>
321
+ <th>Est. Size</th>
322
+ <th>LOC</th>
323
+ <th>Dependencies</th>
324
+ <th>Exports</th>
325
+ <th>Impact</th>
326
+ </tr>
327
+ </thead>
328
+ <tbody>
329
+ ${analysis.components
330
+ .map(c => `
331
+ <tr>
332
+ <td><strong>${c.name}</strong></td>
333
+ <td>${formatSize(c.estimatedSize)}</td>
334
+ <td>${c.complexity}</td>
335
+ <td>${c.dependencies}</td>
336
+ <td>${c.exports}</td>
337
+ <td>
338
+ <span class="impact-badge impact-${c.impact}">${c.impact.toUpperCase()}</span>
339
+ </td>
340
+ </tr>
341
+ `)
342
+ .join('')}
343
+ </tbody>
344
+ </table>
345
+
346
+ <h2 style="margin-top: 40px; color: #333;">🏆 Largest Components</h2>
347
+ <table>
348
+ <thead>
349
+ <tr>
350
+ <th>Rank</th>
351
+ <th>Component</th>
352
+ <th>Size</th>
353
+ <th>% of Total</th>
354
+ </tr>
355
+ </thead>
356
+ <tbody>
357
+ ${analysis.largestComponents
358
+ .map((c, i) => `
359
+ <tr>
360
+ <td><strong>#${i + 1}</strong></td>
361
+ <td>${c.name}</td>
362
+ <td>${formatSize(c.estimatedSize)}</td>
363
+ <td>
364
+ ${((c.estimatedSize / analysis.totalEstimatedSize) * 100).toFixed(1)}%
365
+ <div class="size-bar" style="width: ${((c.estimatedSize / analysis.totalEstimatedSize) * 200).toFixed(0)}px;"></div>
366
+ </td>
367
+ </tr>
368
+ `)
369
+ .join('')}
370
+ </tbody>
371
+ </table>
372
+ </div>
373
+ </body>
374
+ </html>`;
375
+ }
package/dist/cli.js CHANGED
@@ -9,6 +9,10 @@ import { generateHTMLReport } from './htmlReporter.js';
9
9
  import { fixFile, isFixable } from './fixer.js';
10
10
  import { startWatch } from './watcher.js';
11
11
  import { getAllModifiedFiles, filterReactFiles, getGitInfo } from './git.js';
12
+ import { initializeBaseline, recordBaseline, formatTrendReport } from './baseline.js';
13
+ import { sendWebhookNotification, getWebhookConfig } from './webhooks.js';
14
+ import { generateDependencyGraphHTML } from './graphGenerator.js';
15
+ import { generateBundleReport } from './bundleAnalyzer.js';
12
16
  import fs from 'fs/promises';
13
17
  const program = new Command();
14
18
  program
@@ -30,6 +34,15 @@ program
30
34
  .option('--include <patterns>', 'Glob patterns to include (comma-separated)')
31
35
  .option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
32
36
  .option('-o, --output <file>', 'Write output to file')
37
+ .option('--baseline', 'Enable baseline tracking and trend analysis', false)
38
+ .option('--slack <url>', 'Slack webhook URL for notifications')
39
+ .option('--discord <url>', 'Discord webhook URL for notifications')
40
+ .option('--webhook <url>', 'Generic webhook URL for notifications')
41
+ .option('--webhook-threshold <number>', 'Only notify if smells exceed this threshold', parseInt)
42
+ .option('--graph', 'Generate dependency graph visualization', false)
43
+ .option('--graph-format <format>', 'Graph output format: svg, html', 'html')
44
+ .option('--bundle', 'Analyze bundle size impact per component', false)
45
+ .option('--rules <file>', 'Custom rules configuration file')
33
46
  .action(async (directory, options) => {
34
47
  const rootDir = path.resolve(process.cwd(), directory);
35
48
  // Check if directory exists
@@ -53,6 +66,18 @@ program
53
66
  process.exit(1);
54
67
  }
55
68
  }
69
+ // Load custom rules if specified
70
+ let customRules;
71
+ if (options.rules) {
72
+ try {
73
+ const rulesPath = path.resolve(process.cwd(), options.rules);
74
+ const rulesContent = await fs.readFile(rulesPath, 'utf-8');
75
+ customRules = JSON.parse(rulesContent);
76
+ }
77
+ catch (error) {
78
+ console.warn(chalk.yellow(`Warning: Could not load custom rules: ${error.message}`));
79
+ }
80
+ }
56
81
  // Build config from options
57
82
  const config = {
58
83
  ...DEFAULT_CONFIG,
@@ -60,6 +85,10 @@ program
60
85
  ...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
61
86
  ...(options.maxProps && { maxPropsCount: options.maxProps }),
62
87
  ...(options.maxLines && { maxComponentLines: options.maxLines }),
88
+ ...(options.graph && { generateDependencyGraph: true }),
89
+ ...(options.graphFormat && { graphOutputFormat: options.graphFormat }),
90
+ ...(options.bundle && { analyzeBundleSize: true }),
91
+ ...(customRules && { customRules }),
63
92
  };
64
93
  const include = options.include?.split(',').map((p) => p.trim()) || ['**/*.tsx', '**/*.jsx'];
65
94
  const exclude = options.exclude?.split(',').map((p) => p.trim()) || ['**/node_modules/**', '**/dist/**'];
@@ -104,6 +133,10 @@ program
104
133
  config,
105
134
  });
106
135
  spinner.stop();
136
+ // Initialize baseline if enabled
137
+ if (options.baseline) {
138
+ initializeBaseline(rootDir);
139
+ }
107
140
  // Fix mode - apply auto-fixes
108
141
  if (options.fix) {
109
142
  const fixableSmells = result.files.flatMap(f => f.smells.filter(isFixable).map(s => ({ ...s, file: f.file })));
@@ -156,6 +189,47 @@ program
156
189
  else {
157
190
  console.log(output);
158
191
  }
192
+ // Record baseline and show trend analysis
193
+ if (options.baseline) {
194
+ const gitInfo = getGitInfo(rootDir);
195
+ const baselineRecord = recordBaseline(rootDir, result.files.flatMap(f => f.smells), gitInfo.currentCommit);
196
+ console.log(formatTrendReport(rootDir));
197
+ }
198
+ // Send webhook notification
199
+ const webhookConfig = getWebhookConfig(options.slack, options.discord, options.webhook);
200
+ if (webhookConfig) {
201
+ webhookConfig.threshold = options.webhookThreshold;
202
+ const gitInfo = getGitInfo(rootDir);
203
+ const metadata = {
204
+ branch: gitInfo.branch,
205
+ commit: gitInfo.currentCommit,
206
+ author: gitInfo.authorName,
207
+ };
208
+ const sent = await sendWebhookNotification(webhookConfig, result.files.flatMap(f => f.smells), path.basename(rootDir), metadata);
209
+ if (sent) {
210
+ console.log(chalk.green('✓ Notification sent to webhook'));
211
+ }
212
+ }
213
+ // Generate dependency graph if requested
214
+ if (options.graph) {
215
+ const graph = global._dependencyGraph;
216
+ if (graph) {
217
+ const graphHTML = generateDependencyGraphHTML(graph, path.basename(rootDir), graph.circularDependencies.length);
218
+ const graphPath = path.resolve(process.cwd(), 'dependency-graph.html');
219
+ await fs.writeFile(graphPath, graphHTML, 'utf-8');
220
+ console.log(chalk.green(`✓ Dependency graph written to ${graphPath}`));
221
+ }
222
+ }
223
+ // Generate bundle analysis if requested
224
+ if (options.bundle) {
225
+ const bundleAnalysis = global._bundleAnalysis;
226
+ if (bundleAnalysis) {
227
+ const bundleHTML = generateBundleReport(bundleAnalysis, path.basename(rootDir));
228
+ const bundlePath = path.resolve(process.cwd(), 'bundle-analysis.html');
229
+ await fs.writeFile(bundlePath, bundleHTML, 'utf-8');
230
+ console.log(chalk.green(`✓ Bundle analysis written to ${bundlePath}`));
231
+ }
232
+ }
159
233
  // CI/CD exit code handling
160
234
  const { smellsBySeverity } = result.summary;
161
235
  let shouldFail = false;
@@ -0,0 +1,31 @@
1
+ import { CodeSmell } from './types/index.js';
2
+ export interface CustomRule {
3
+ name: string;
4
+ description?: string;
5
+ severity: 'error' | 'warning' | 'info';
6
+ pattern: string | RegExp;
7
+ patternType: 'regex' | 'ast' | 'text';
8
+ message?: string;
9
+ enabled?: boolean;
10
+ }
11
+ export interface CustomRulesConfig {
12
+ rules: CustomRule[];
13
+ enabled?: boolean;
14
+ }
15
+ /**
16
+ * Parse custom rules from configuration
17
+ */
18
+ export declare function parseCustomRules(config: any): CustomRule[];
19
+ /**
20
+ * Detect violations of custom rules
21
+ */
22
+ export declare function detectCustomRuleViolations(component: any, filePath: string, sourceCode: string, customRules: CustomRule[]): CodeSmell[];
23
+ /**
24
+ * Example custom rules configuration
25
+ */
26
+ export declare const EXAMPLE_CUSTOM_RULES: CustomRulesConfig;
27
+ /**
28
+ * Generate custom rules documentation
29
+ */
30
+ export declare function generateRulesDocumentation(): string;
31
+ //# sourceMappingURL=customRules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customRules.d.ts","sourceRoot":"","sources":["../src/customRules.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAK7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,WAAW,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,GAAG,GAAG,UAAU,EAAE,CAY1D;AAsBD;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,UAAU,EAAE,GACxB,SAAS,EAAE,CA2Bb;AAuDD;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAoClC,CAAC;AAEF;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CA8InD"}