ucn 3.1.8 → 3.3.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.

Potentially problematic release.


This version of ucn might be problematic. Click here for more details.

package/core/imports.js CHANGED
@@ -9,49 +9,6 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { getParser, getLanguageModule } = require('../languages');
11
11
 
12
- /**
13
- * Import patterns by language
14
- * @deprecated Use AST-based findImportsInCode() from language modules instead.
15
- * Kept only as fallback for unsupported languages or when AST parsing fails.
16
- */
17
- const IMPORT_PATTERNS = {
18
- javascript: {
19
- importDefault: /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
20
- importNamed: /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
21
- importNamespace: /import\s*\*\s*as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
22
- require: /(?:const|let|var)\s+(?:\{[^}]+\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
23
- exportNamed: /^\s*export\s+(?:async\s+)?(?:function|class|const|let|var|interface|type)\s+(\w+)/gm,
24
- exportDefault: /^\s*export\s+default\s+(?:(?:async\s+)?(?:function|class)\s+)?(\w+)?/gm,
25
- exportList: /^\s*export\s*\{([^}]+)\}/gm,
26
- moduleExports: /^module\.exports\s*=\s*(?:\{([^}]+)\}|(\w+))/gm,
27
- exportsNamed: /^exports\.(\w+)\s*=[^=]/gm,
28
- importType: /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
29
- importSideEffect: /import\s+['"]([^'"]+)['"]/g,
30
- importDynamic: /(?:await\s+)?import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
31
- reExportNamed: /^\s*export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/gm,
32
- reExportAll: /^\s*export\s*\*\s*from\s*['"]([^'"]+)['"]/gm
33
- },
34
- python: {
35
- importModule: /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm,
36
- fromImport: /^from\s+([.\w]+)\s+import\s+(.+)/gm,
37
- exportAll: /__all__\s*=\s*\[([^\]]+)\]/g
38
- },
39
- go: {
40
- importSingle: /import\s+"([^"]+)"/g,
41
- importBlock: /import\s*\(\s*([\s\S]*?)\s*\)/g,
42
- exportedFunc: /^func\s+(?:\([^)]+\)\s+)?([A-Z]\w*)\s*\(/gm,
43
- exportedType: /^type\s+([A-Z]\w*)\s+/gm
44
- },
45
- java: {
46
- importStatement: /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g,
47
- exportedClass: /public\s+(?:abstract\s+)?(?:final\s+)?(?:class|interface|enum)\s+(\w+)/g
48
- },
49
- rust: {
50
- useStatement: /^use\s+([^;]+);/gm,
51
- modDecl: /^\s*mod\s+(\w+)\s*;/gm
52
- }
53
- };
54
-
55
12
  /**
56
13
  * Extract imports from file content using AST
57
14
  *
@@ -63,186 +20,21 @@ function extractImports(content, language) {
63
20
  // Normalize language name for parser
64
21
  const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
65
22
 
66
- // Try AST-based extraction first
67
23
  const langModule = getLanguageModule(normalizedLang);
68
24
  if (langModule && typeof langModule.findImportsInCode === 'function') {
69
25
  try {
70
26
  const parser = getParser(normalizedLang);
71
27
  if (parser) {
72
28
  const imports = langModule.findImportsInCode(content, parser);
73
- return { imports };
29
+ const dynamicCount = imports.filter(i => i.dynamic).length;
30
+ return { imports, dynamicCount };
74
31
  }
75
32
  } catch (e) {
76
- // Fall through to regex-based extraction
33
+ // AST parsing failed
77
34
  }
78
35
  }
79
36
 
80
- // Fallback to regex-based extraction (deprecated)
81
- const imports = [];
82
- if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
83
- extractJSImports(content, imports);
84
- } else if (language === 'python') {
85
- extractPythonImports(content, imports);
86
- } else if (language === 'go') {
87
- extractGoImports(content, imports);
88
- } else if (language === 'java') {
89
- extractJavaImports(content, imports);
90
- } else if (language === 'rust') {
91
- extractRustImports(content, imports);
92
- }
93
-
94
- return { imports };
95
- }
96
-
97
- /**
98
- * @deprecated Use AST-based findImportsInCode() from language modules.
99
- */
100
- function extractJSImports(content, imports) {
101
- const patterns = IMPORT_PATTERNS.javascript;
102
- let match;
103
-
104
- // Default imports
105
- let regex = new RegExp(patterns.importDefault.source, 'g');
106
- while ((match = regex.exec(content)) !== null) {
107
- imports.push({ module: match[2], names: [match[1]], type: 'default' });
108
- }
109
-
110
- // Named imports
111
- regex = new RegExp(patterns.importNamed.source, 'g');
112
- while ((match = regex.exec(content)) !== null) {
113
- const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
114
- imports.push({ module: match[2], names, type: 'named' });
115
- }
116
-
117
- // Namespace imports
118
- regex = new RegExp(patterns.importNamespace.source, 'g');
119
- while ((match = regex.exec(content)) !== null) {
120
- imports.push({ module: match[2], names: [match[1]], type: 'namespace' });
121
- }
122
-
123
- // Require
124
- regex = new RegExp(patterns.require.source, 'g');
125
- while ((match = regex.exec(content)) !== null) {
126
- imports.push({ module: match[2], names: match[1] ? [match[1]] : [], type: 'require' });
127
- }
128
-
129
- // Type imports
130
- regex = new RegExp(patterns.importType.source, 'g');
131
- while ((match = regex.exec(content)) !== null) {
132
- const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
133
- imports.push({ module: match[2], names, type: 'type' });
134
- }
135
-
136
- // Side-effect imports
137
- regex = new RegExp(patterns.importSideEffect.source, 'g');
138
- while ((match = regex.exec(content)) !== null) {
139
- const module = match[1];
140
- if (!imports.some(i => i.module === module)) {
141
- imports.push({ module, names: [], type: 'side-effect' });
142
- }
143
- }
144
-
145
- // Dynamic imports
146
- regex = new RegExp(patterns.importDynamic.source, 'g');
147
- while ((match = regex.exec(content)) !== null) {
148
- const module = match[1];
149
- if (!imports.some(i => i.module === module)) {
150
- imports.push({ module, names: [], type: 'dynamic' });
151
- }
152
- }
153
-
154
- // Re-exports
155
- regex = new RegExp(patterns.reExportNamed.source, 'gm');
156
- while ((match = regex.exec(content)) !== null) {
157
- const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
158
- imports.push({ module: match[2], names, type: 're-export' });
159
- }
160
-
161
- regex = new RegExp(patterns.reExportAll.source, 'gm');
162
- while ((match = regex.exec(content)) !== null) {
163
- imports.push({ module: match[1], names: ['*'], type: 're-export-all' });
164
- }
165
- }
166
-
167
- /** @deprecated Use AST-based findImportsInCode() from language modules. */
168
- function extractPythonImports(content, imports) {
169
- const patterns = IMPORT_PATTERNS.python;
170
- let match;
171
-
172
- let regex = new RegExp(patterns.importModule.source, 'gm');
173
- while ((match = regex.exec(content)) !== null) {
174
- const moduleName = match[1];
175
- const alias = match[2] || moduleName.split('.').pop();
176
- imports.push({ module: moduleName, names: [alias], type: 'module' });
177
- }
178
-
179
- regex = new RegExp(patterns.fromImport.source, 'gm');
180
- while ((match = regex.exec(content)) !== null) {
181
- const moduleName = match[1];
182
- const importList = match[2].trim();
183
-
184
- if (importList === '*') {
185
- imports.push({ module: moduleName, names: ['*'], type: 'star' });
186
- } else {
187
- const names = importList.split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n && n !== '(');
188
- imports.push({ module: moduleName, names, type: 'from' });
189
- }
190
- }
191
- }
192
-
193
- /** @deprecated Use AST-based findImportsInCode() from language modules. */
194
- function extractGoImports(content, imports) {
195
- const patterns = IMPORT_PATTERNS.go;
196
- let match;
197
-
198
- let regex = new RegExp(patterns.importSingle.source, 'g');
199
- while ((match = regex.exec(content)) !== null) {
200
- const pkg = match[1];
201
- imports.push({ module: pkg, names: [path.basename(pkg)], type: 'single' });
202
- }
203
-
204
- regex = new RegExp(patterns.importBlock.source, 'g');
205
- while ((match = regex.exec(content)) !== null) {
206
- const block = match[1];
207
- const pkgMatches = block.matchAll(/"([^"]+)"/g);
208
- for (const pkgMatch of pkgMatches) {
209
- const pkg = pkgMatch[1];
210
- imports.push({ module: pkg, names: [path.basename(pkg)], type: 'block' });
211
- }
212
- }
213
- }
214
-
215
- /** @deprecated Use AST-based findImportsInCode() from language modules. */
216
- function extractJavaImports(content, imports) {
217
- const patterns = IMPORT_PATTERNS.java;
218
- let match;
219
-
220
- let regex = new RegExp(patterns.importStatement.source, 'g');
221
- while ((match = regex.exec(content)) !== null) {
222
- const fullImport = match[1];
223
- const parts = fullImport.split('.');
224
- const name = parts[parts.length - 1];
225
- imports.push({ module: fullImport, names: name === '*' ? ['*'] : [name], type: 'import' });
226
- }
227
- }
228
-
229
- /** @deprecated Use AST-based findImportsInCode() from language modules. */
230
- function extractRustImports(content, imports) {
231
- const patterns = IMPORT_PATTERNS.rust;
232
- let match;
233
-
234
- let regex = new RegExp(patterns.useStatement.source, 'gm');
235
- while ((match = regex.exec(content)) !== null) {
236
- let raw = match[1].trim().split('{')[0].trim().split(' as ')[0].trim().replace(/::$/, '');
237
- if (raw) {
238
- imports.push({ module: raw, names: [], type: 'use' });
239
- }
240
- }
241
-
242
- regex = new RegExp(patterns.modDecl.source, 'gm');
243
- while ((match = regex.exec(content)) !== null) {
244
- imports.push({ module: `self::${match[1]}`, names: [match[1]], type: 'mod' });
245
- }
37
+ return { imports: [], dynamicCount: 0 };
246
38
  }
247
39
 
248
40
  /**
@@ -252,7 +44,6 @@ function extractExports(content, language) {
252
44
  // Normalize language name for parser
253
45
  const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
254
46
 
255
- // Try AST-based extraction first
256
47
  const langModule = getLanguageModule(normalizedLang);
257
48
  if (langModule && typeof langModule.findExportsInCode === 'function') {
258
49
  try {
@@ -262,123 +53,11 @@ function extractExports(content, language) {
262
53
  return { exports: foundExports };
263
54
  }
264
55
  } catch (e) {
265
- // Fall through to regex-based extraction
266
- }
267
- }
268
-
269
- // Fallback to regex-based extraction (deprecated)
270
- const foundExports = [];
271
- if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
272
- extractJSExports(content, foundExports);
273
- } else if (language === 'python') {
274
- extractPythonExports(content, foundExports);
275
- } else if (language === 'go') {
276
- extractGoExports(content, foundExports);
277
- } else if (language === 'java') {
278
- extractJavaExports(content, foundExports);
279
- }
280
-
281
- return { exports: foundExports };
282
- }
283
-
284
- /** @deprecated Use AST-based findExportsInCode() from language modules. */
285
- function extractJSExports(content, exports) {
286
- const patterns = IMPORT_PATTERNS.javascript;
287
- let match;
288
-
289
- let regex = new RegExp(patterns.exportNamed.source, 'gm');
290
- while ((match = regex.exec(content)) !== null) {
291
- exports.push({ name: match[1], type: 'named' });
292
- }
293
-
294
- regex = new RegExp(patterns.exportDefault.source, 'gm');
295
- while ((match = regex.exec(content)) !== null) {
296
- exports.push({ name: match[1] || 'default', type: 'default' });
297
- }
298
-
299
- regex = new RegExp(patterns.exportList.source, 'gm');
300
- while ((match = regex.exec(content)) !== null) {
301
- const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
302
- for (const name of names) {
303
- exports.push({ name, type: 'list' });
304
- }
305
- }
306
-
307
- regex = new RegExp(patterns.exportsNamed.source, 'gm');
308
- while ((match = regex.exec(content)) !== null) {
309
- exports.push({ name: match[1], type: 'commonjs-named' });
310
- }
311
-
312
- // module.exports = { a, b, c } or module.exports = identifier
313
- regex = new RegExp(patterns.moduleExports.source, 'gm');
314
- while ((match = regex.exec(content)) !== null) {
315
- if (match[1]) {
316
- // Object literal: module.exports = { a, b, c }
317
- const names = match[1].split(',').map(n => n.trim().split(/\s*:\s*/)[0].trim()).filter(n => n && !n.includes('('));
318
- for (const name of names) {
319
- exports.push({ name, type: 'commonjs-object' });
320
- }
321
- } else if (match[2]) {
322
- // Single identifier: module.exports = SomeClass
323
- exports.push({ name: match[2], type: 'commonjs-default' });
324
- }
325
- }
326
- }
327
-
328
- /** @deprecated Use AST-based findExportsInCode() from language modules. */
329
- function extractPythonExports(content, exports) {
330
- let match;
331
-
332
- // Check for __all__
333
- let regex = new RegExp(IMPORT_PATTERNS.python.exportAll.source, 'g');
334
- while ((match = regex.exec(content)) !== null) {
335
- const names = match[1].split(',').map(n => n.trim().replace(/['"]/g, '')).filter(n => n);
336
- for (const name of names) {
337
- exports.push({ name, type: 'explicit' });
56
+ // AST parsing failed
338
57
  }
339
58
  }
340
59
 
341
- // If no __all__, look for public names
342
- if (exports.length === 0) {
343
- const funcRegex = /^def\s+([a-zA-Z]\w*)\s*\(/gm;
344
- while ((match = funcRegex.exec(content)) !== null) {
345
- if (!match[1].startsWith('_')) {
346
- exports.push({ name: match[1], type: 'function' });
347
- }
348
- }
349
-
350
- const classRegex = /^class\s+([a-zA-Z]\w*)/gm;
351
- while ((match = classRegex.exec(content)) !== null) {
352
- if (!match[1].startsWith('_')) {
353
- exports.push({ name: match[1], type: 'class' });
354
- }
355
- }
356
- }
357
- }
358
-
359
- /** @deprecated Use AST-based findExportsInCode() from language modules. */
360
- function extractGoExports(content, exports) {
361
- const patterns = IMPORT_PATTERNS.go;
362
- let match;
363
-
364
- let regex = new RegExp(patterns.exportedFunc.source, 'gm');
365
- while ((match = regex.exec(content)) !== null) {
366
- exports.push({ name: match[1], type: 'function' });
367
- }
368
-
369
- regex = new RegExp(patterns.exportedType.source, 'gm');
370
- while ((match = regex.exec(content)) !== null) {
371
- exports.push({ name: match[1], type: 'type' });
372
- }
373
- }
374
-
375
- /** @deprecated Use AST-based findExportsInCode() from language modules. */
376
- function extractJavaExports(content, exports) {
377
- let match;
378
- let regex = new RegExp(IMPORT_PATTERNS.java.exportedClass.source, 'g');
379
- while ((match = regex.exec(content)) !== null) {
380
- exports.push({ name: match[1], type: 'class' });
381
- }
60
+ return { exports: [] };
382
61
  }
383
62
 
384
63
  // Cache for tsconfig lookups
@@ -437,11 +116,37 @@ function resolveImport(importPath, fromFile, config = {}) {
437
116
  if (resolved) return resolved;
438
117
  }
439
118
 
119
+ // Rust: crate::, super::, self:: paths and mod declarations
120
+ if (config.language === 'rust') {
121
+ const resolved = resolveRustImport(importPath, fromFile, config.root);
122
+ if (resolved) return resolved;
123
+ }
124
+
440
125
  return null; // External package
441
126
  }
442
127
 
128
+ // Python relative imports: translate dot-prefix notation to file paths
129
+ // e.g., ".models" -> "./models", "..utils" -> "../utils", "." -> "."
130
+ let normalizedPath = importPath;
131
+ if (config.language === 'python') {
132
+ // Count leading dots and convert to filesystem relative path
133
+ const dotMatch = importPath.match(/^(\.+)(.*)/);
134
+ if (dotMatch) {
135
+ const dots = dotMatch[1];
136
+ const rest = dotMatch[2];
137
+ if (dots.length === 1) {
138
+ // ".models" -> "./models", "." -> "."
139
+ normalizedPath = rest ? './' + rest.replace(/\./g, '/') : '.';
140
+ } else {
141
+ // "..models" -> "../models", "...models" -> "../../models"
142
+ const upDirs = '../'.repeat(dots.length - 1);
143
+ normalizedPath = rest ? upDirs + rest.replace(/\./g, '/') : upDirs.slice(0, -1);
144
+ }
145
+ }
146
+ }
147
+
443
148
  // Relative imports
444
- const resolved = path.resolve(fromDir, importPath);
149
+ const resolved = path.resolve(fromDir, normalizedPath);
445
150
  return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
446
151
  }
447
152
 
@@ -522,6 +227,125 @@ function resolveGoImport(importPath, fromFile, projectRoot) {
522
227
  return null;
523
228
  }
524
229
 
230
+ // Cache for Rust crate roots (Cargo.toml locations)
231
+ const cargoCache = new Map();
232
+
233
+ /**
234
+ * Find the nearest Cargo.toml and return the crate's source root
235
+ * @param {string} startDir - Directory to start searching from
236
+ * @returns {{root: string, srcDir: string}|null}
237
+ */
238
+ function findCargoRoot(startDir) {
239
+ if (cargoCache.has(startDir)) {
240
+ return cargoCache.get(startDir);
241
+ }
242
+
243
+ let dir = startDir;
244
+ while (dir !== path.dirname(dir)) {
245
+ const cargoPath = path.join(dir, 'Cargo.toml');
246
+ if (fs.existsSync(cargoPath)) {
247
+ const srcDir = path.join(dir, 'src');
248
+ const result = fs.existsSync(srcDir) ? { root: dir, srcDir } : null;
249
+ cargoCache.set(startDir, result);
250
+ return result;
251
+ }
252
+ dir = path.dirname(dir);
253
+ }
254
+
255
+ cargoCache.set(startDir, null);
256
+ return null;
257
+ }
258
+
259
+ /**
260
+ * Try to resolve a Rust module path to a file
261
+ * Checks both <path>.rs and <path>/mod.rs
262
+ * @param {string} dir - Base directory
263
+ * @param {string[]} segments - Path segments to resolve
264
+ * @returns {string|null}
265
+ */
266
+ function resolveRustModulePath(dir, segments) {
267
+ // Try progressively shorter paths (items at the end may be types, not modules)
268
+ for (let len = segments.length; len >= 1; len--) {
269
+ const modPath = path.join(dir, ...segments.slice(0, len));
270
+ // Try <path>.rs
271
+ const rsFile = modPath + '.rs';
272
+ if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
273
+ return rsFile;
274
+ }
275
+ // Try <path>/mod.rs
276
+ const modFile = path.join(modPath, 'mod.rs');
277
+ if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
278
+ return modFile;
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+
284
+ /**
285
+ * Resolve Rust import paths to local files
286
+ * Handles: crate::, super::, self::, and mod declarations
287
+ * @param {string} importPath - Rust import path (e.g., "crate::display::Display" or "display")
288
+ * @param {string} fromFile - File containing the import
289
+ * @param {string} projectRoot - Project root directory
290
+ * @returns {string|null}
291
+ */
292
+ function resolveRustImport(importPath, fromFile, projectRoot) {
293
+ const fromDir = path.dirname(fromFile);
294
+
295
+ // crate:: paths - resolve from the crate's src/ directory
296
+ if (importPath.startsWith('crate::')) {
297
+ const cargo = findCargoRoot(fromDir);
298
+ if (!cargo) return null;
299
+
300
+ const rest = importPath.slice('crate::'.length);
301
+ const segments = rest.split('::');
302
+ return resolveRustModulePath(cargo.srcDir, segments);
303
+ }
304
+
305
+ // super:: paths - resolve relative to parent directory
306
+ if (importPath.startsWith('super::')) {
307
+ let dir = fromDir;
308
+ let rest = importPath;
309
+ while (rest.startsWith('super::')) {
310
+ // If current file is mod.rs, go up one more directory
311
+ const basename = path.basename(fromFile);
312
+ if (basename === 'mod.rs' && dir === fromDir) {
313
+ dir = path.dirname(dir);
314
+ }
315
+ dir = path.dirname(dir);
316
+ rest = rest.slice('super::'.length);
317
+ }
318
+ const segments = rest.split('::');
319
+ return resolveRustModulePath(dir, segments);
320
+ }
321
+
322
+ // self:: paths - resolve within current module directory
323
+ if (importPath.startsWith('self::')) {
324
+ const rest = importPath.slice('self::'.length);
325
+ const segments = rest.split('::');
326
+ // If current file is mod.rs, resolve relative to its directory
327
+ const basename = path.basename(fromFile);
328
+ const dir = basename === 'mod.rs' ? fromDir : path.dirname(fromDir);
329
+ return resolveRustModulePath(dir, segments);
330
+ }
331
+
332
+ // Plain module name without :: (potential mod declaration)
333
+ // e.g., "display" from `mod display;` - resolve relative to declaring file
334
+ if (!importPath.includes('::')) {
335
+ // For mod declarations: <dir>/<name>.rs or <dir>/<name>/mod.rs
336
+ const rsFile = path.join(fromDir, importPath + '.rs');
337
+ if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
338
+ return rsFile;
339
+ }
340
+ const modFile = path.join(fromDir, importPath, 'mod.rs');
341
+ if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
342
+ return modFile;
343
+ }
344
+ }
345
+
346
+ return null;
347
+ }
348
+
525
349
  /**
526
350
  * Try to resolve a path with various extensions
527
351
  */
@@ -537,11 +361,14 @@ function resolveFilePath(basePath, extensions) {
537
361
  if (fs.existsSync(withExt)) return withExt;
538
362
  }
539
363
 
540
- // Try index files
364
+ // Try index files (index.js for JS/TS, __init__.py for Python)
541
365
  for (const ext of extensions) {
542
366
  const indexPath = path.join(basePath, 'index' + ext);
543
367
  if (fs.existsSync(indexPath)) return indexPath;
544
368
  }
369
+ // Python __init__.py
370
+ const initPath = path.join(basePath, '__init__.py');
371
+ if (fs.existsSync(initPath)) return initPath;
545
372
 
546
373
  return null;
547
374
  }
@@ -636,6 +463,5 @@ function stripJsonComments(content) {
636
463
  module.exports = {
637
464
  extractImports,
638
465
  extractExports,
639
- resolveImport,
640
- IMPORT_PATTERNS
466
+ resolveImport
641
467
  };