ucn 3.1.0 → 3.1.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.
package/cli/index.js CHANGED
@@ -745,7 +745,7 @@ function runProjectCommand(rootDir, command, arg) {
745
745
 
746
746
  case 'context': {
747
747
  requireArg(arg, 'Usage: ucn . context <name>');
748
- const ctx = index.context(arg, { includeMethods: flags.includeMethods });
748
+ const ctx = index.context(arg, { includeMethods: flags.includeMethods, file: flags.file });
749
749
  printOutput(ctx,
750
750
  output.formatContextJson,
751
751
  r => { printContext(r, { expand: flags.expand, root: index.root }); }
@@ -787,7 +787,7 @@ function runProjectCommand(rootDir, command, arg) {
787
787
 
788
788
  case 'about': {
789
789
  requireArg(arg, 'Usage: ucn . about <name>');
790
- const aboutResult = index.about(arg, { withTypes: flags.withTypes });
790
+ const aboutResult = index.about(arg, { withTypes: flags.withTypes, file: flags.file });
791
791
  printOutput(aboutResult,
792
792
  output.formatAboutJson,
793
793
  r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth })
@@ -797,7 +797,7 @@ function runProjectCommand(rootDir, command, arg) {
797
797
 
798
798
  case 'impact': {
799
799
  requireArg(arg, 'Usage: ucn . impact <name>');
800
- const impactResult = index.impact(arg);
800
+ const impactResult = index.impact(arg, { file: flags.file });
801
801
  printOutput(impactResult, output.formatImpactJson, output.formatImpact);
802
802
  break;
803
803
  }
@@ -821,7 +821,7 @@ function runProjectCommand(rootDir, command, arg) {
821
821
  case 'trace': {
822
822
  requireArg(arg, 'Usage: ucn . trace <name>');
823
823
  const traceDepth = flags.depth ? parseInt(flags.depth) : 3;
824
- const traceResult = index.trace(arg, { depth: traceDepth });
824
+ const traceResult = index.trace(arg, { depth: traceDepth, file: flags.file });
825
825
  printOutput(traceResult, output.formatTraceJson, output.formatTrace);
826
826
  break;
827
827
  }
package/core/imports.js CHANGED
@@ -431,6 +431,12 @@ function resolveImport(importPath, fromFile, config = {}) {
431
431
  }
432
432
  }
433
433
 
434
+ // Check Go module imports
435
+ if (config.language === 'go') {
436
+ const resolved = resolveGoImport(importPath, fromFile, config.root);
437
+ if (resolved) return resolved;
438
+ }
439
+
434
440
  return null; // External package
435
441
  }
436
442
 
@@ -439,6 +445,83 @@ function resolveImport(importPath, fromFile, config = {}) {
439
445
  return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
440
446
  }
441
447
 
448
+ // Cache for Go module paths
449
+ const goModuleCache = new Map();
450
+
451
+ /**
452
+ * Find and parse go.mod to get the module path
453
+ * @param {string} startDir - Directory to start searching from
454
+ * @returns {{modulePath: string, root: string}|null}
455
+ */
456
+ function findGoModule(startDir) {
457
+ // Check cache first
458
+ if (goModuleCache.has(startDir)) {
459
+ return goModuleCache.get(startDir);
460
+ }
461
+
462
+ let dir = startDir;
463
+ while (dir !== path.dirname(dir)) {
464
+ const goModPath = path.join(dir, 'go.mod');
465
+ if (fs.existsSync(goModPath)) {
466
+ try {
467
+ const content = fs.readFileSync(goModPath, 'utf-8');
468
+ // Parse module line: module github.com/user/project
469
+ const match = content.match(/^module\s+(\S+)/m);
470
+ if (match) {
471
+ const result = { modulePath: match[1], root: dir };
472
+ goModuleCache.set(startDir, result);
473
+ return result;
474
+ }
475
+ } catch (e) {
476
+ // Ignore read errors
477
+ }
478
+ }
479
+ dir = path.dirname(dir);
480
+ }
481
+
482
+ goModuleCache.set(startDir, null);
483
+ return null;
484
+ }
485
+
486
+ /**
487
+ * Resolve Go package import to local files
488
+ * @param {string} importPath - Go import path (e.g., "github.com/user/proj/pkg/util")
489
+ * @param {string} fromFile - File containing the import
490
+ * @param {string} projectRoot - Project root directory
491
+ * @returns {string|null} - Directory path containing the package, or null if external
492
+ */
493
+ function resolveGoImport(importPath, fromFile, projectRoot) {
494
+ const goMod = findGoModule(path.dirname(fromFile));
495
+ if (!goMod) return null;
496
+
497
+ const { modulePath, root } = goMod;
498
+
499
+ // Check if the import is within this module
500
+ if (importPath.startsWith(modulePath)) {
501
+ // Convert module path to relative path
502
+ // e.g., "github.com/user/proj/pkg/util" -> "pkg/util"
503
+ const relativePath = importPath.slice(modulePath.length).replace(/^\//, '');
504
+ const pkgDir = path.join(root, relativePath);
505
+
506
+ // Go imports are directories, find a .go file in the directory
507
+ if (fs.existsSync(pkgDir) && fs.statSync(pkgDir).isDirectory()) {
508
+ // Return the first .go file in the directory (not _test.go)
509
+ try {
510
+ const files = fs.readdirSync(pkgDir);
511
+ for (const file of files) {
512
+ if (file.endsWith('.go') && !file.endsWith('_test.go')) {
513
+ return path.join(pkgDir, file);
514
+ }
515
+ }
516
+ } catch (e) {
517
+ // Ignore read errors
518
+ }
519
+ }
520
+ }
521
+
522
+ return null;
523
+ }
524
+
442
525
  /**
443
526
  * Try to resolve a path with various extensions
444
527
  */
package/core/project.js CHANGED
@@ -706,11 +706,21 @@ class ProjectIndex {
706
706
  * Get context for a symbol (callers + callees)
707
707
  */
708
708
  context(name, options = {}) {
709
- const definitions = this.symbols.get(name) || [];
709
+ let definitions = this.symbols.get(name) || [];
710
710
  if (definitions.length === 0) {
711
711
  return { function: name, file: null, callers: [], callees: [] };
712
712
  }
713
713
 
714
+ // Filter by file if specified
715
+ if (options.file) {
716
+ const filtered = definitions.filter(d =>
717
+ d.relativePath && d.relativePath.includes(options.file)
718
+ );
719
+ if (filtered.length > 0) {
720
+ definitions = filtered;
721
+ }
722
+ }
723
+
714
724
  // Prefer class/struct/interface definitions over functions/methods/constructors
715
725
  // This ensures context('ClassName') finds the class, not a constructor with same name
716
726
  const typeOrder = ['class', 'struct', 'interface', 'type', 'impl'];
@@ -974,6 +984,10 @@ class ProjectIndex {
974
984
  const calls = this.getCachedCalls(def.file);
975
985
  if (!calls) return [];
976
986
 
987
+ // Get file language for smart method call handling
988
+ const fileEntry = this.files.get(def.file);
989
+ const language = fileEntry?.language;
990
+
977
991
  const callees = new Map(); // name -> count
978
992
 
979
993
  for (const call of calls) {
@@ -982,8 +996,12 @@ class ProjectIndex {
982
996
  if (call.enclosingFunction.name !== def.name) continue;
983
997
  if (call.enclosingFunction.startLine !== def.startLine) continue;
984
998
 
985
- // Skip method calls unless explicitly requested
986
- if (call.isMethod && !options.includeMethods) continue;
999
+ // Smart method call handling:
1000
+ // - Go: include all method calls (Go doesn't use this/self/cls)
1001
+ // - Other languages: skip method calls unless explicitly requested
1002
+ if (call.isMethod) {
1003
+ if (language !== 'go' && !options.includeMethods) continue;
1004
+ }
987
1005
 
988
1006
  // Skip keywords and built-ins
989
1007
  if (this.isKeyword(call.name)) continue;
@@ -2102,11 +2120,21 @@ class ProjectIndex {
2102
2120
  const maxDepth = Math.max(0, rawDepth);
2103
2121
  const direction = options.direction || 'down'; // 'down' = callees, 'up' = callers, 'both'
2104
2122
 
2105
- const definitions = this.symbols.get(name);
2123
+ let definitions = this.symbols.get(name);
2106
2124
  if (!definitions || definitions.length === 0) {
2107
2125
  return null;
2108
2126
  }
2109
2127
 
2128
+ // Filter by file if specified
2129
+ if (options.file) {
2130
+ const filtered = definitions.filter(d =>
2131
+ d.relativePath && d.relativePath.includes(options.file)
2132
+ );
2133
+ if (filtered.length > 0) {
2134
+ definitions = filtered;
2135
+ }
2136
+ }
2137
+
2110
2138
  const def = definitions[0];
2111
2139
  const visited = new Set();
2112
2140
 
@@ -2184,11 +2212,21 @@ class ProjectIndex {
2184
2212
  * @returns {object} Impact analysis
2185
2213
  */
2186
2214
  impact(name, options = {}) {
2187
- const definitions = this.symbols.get(name);
2215
+ let definitions = this.symbols.get(name);
2188
2216
  if (!definitions || definitions.length === 0) {
2189
2217
  return null;
2190
2218
  }
2191
2219
 
2220
+ // Filter by file if specified
2221
+ if (options.file) {
2222
+ const filtered = definitions.filter(d =>
2223
+ d.relativePath && d.relativePath.includes(options.file)
2224
+ );
2225
+ if (filtered.length > 0) {
2226
+ definitions = filtered;
2227
+ }
2228
+ }
2229
+
2192
2230
  const def = definitions[0];
2193
2231
  const usages = this.usages(name, { codeOnly: true });
2194
2232
  const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
@@ -2899,10 +2937,10 @@ class ProjectIndex {
2899
2937
  const maxCallees = options.maxCallees || 5;
2900
2938
 
2901
2939
  // Find symbol definition(s)
2902
- const definitions = this.find(name, { exact: true });
2940
+ const definitions = this.find(name, { exact: true, file: options.file });
2903
2941
  if (definitions.length === 0) {
2904
2942
  // Try fuzzy match
2905
- const fuzzy = this.find(name);
2943
+ const fuzzy = this.find(name, { file: options.file });
2906
2944
  if (fuzzy.length === 0) {
2907
2945
  return null;
2908
2946
  }
package/languages/go.js CHANGED
@@ -567,8 +567,9 @@ function findUsagesInCode(code, name, parser) {
567
567
  const usages = [];
568
568
 
569
569
  traverseTree(tree.rootNode, (node) => {
570
- // Only look for identifiers with the matching name
571
- if (node.type !== 'identifier' || node.text !== name) {
570
+ // Look for both identifier and field_identifier (method names in selector expressions)
571
+ const isIdentifier = node.type === 'identifier' || node.type === 'field_identifier';
572
+ if (!isIdentifier || node.text !== name) {
572
573
  return true;
573
574
  }
574
575
 
@@ -622,9 +623,8 @@ function findUsagesInCode(code, name, parser) {
622
623
  else if (parent.type === 'parameter_declaration') {
623
624
  usageType = 'definition';
624
625
  }
625
- // Method call: selector_expression followed by call
626
- else if (parent.type === 'selector_expression' &&
627
- parent.childForFieldName('field') === node) {
626
+ // Method call: selector_expression followed by call (field_identifier case)
627
+ else if (parent.type === 'selector_expression') {
628
628
  const grandparent = parent.parent;
629
629
  if (grandparent && grandparent.type === 'call_expression') {
630
630
  usageType = 'call';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.1.0",
4
- "description": "Code navigation built by AI, for AI. Reduces context usage by 90%+ when working with large codebases.",
3
+ "version": "3.1.2",
4
+ "description": "Code navigation built by AI, for AI. Reduces context usage when working with large codebases.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "ucn": "./cli/index.js"
@@ -4297,6 +4297,168 @@ class Product {
4297
4297
  fs.rmSync(tmpDir, { recursive: true, force: true });
4298
4298
  }
4299
4299
  });
4300
+
4301
+ it('should resolve Go module imports for exporters', () => {
4302
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-import-'));
4303
+ try {
4304
+ // Create go.mod file
4305
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), `module example.com/myproject
4306
+
4307
+ go 1.21
4308
+ `);
4309
+
4310
+ // Create package structure
4311
+ fs.mkdirSync(path.join(tmpDir, 'pkg', 'config'), { recursive: true });
4312
+ fs.writeFileSync(path.join(tmpDir, 'pkg', 'config', 'config.go'), `package config
4313
+
4314
+ type Config struct {
4315
+ Name string
4316
+ }
4317
+
4318
+ func NewConfig() *Config {
4319
+ return &Config{}
4320
+ }
4321
+ `);
4322
+
4323
+ fs.writeFileSync(path.join(tmpDir, 'main.go'), `package main
4324
+
4325
+ import "example.com/myproject/pkg/config"
4326
+
4327
+ func main() {
4328
+ cfg := config.NewConfig()
4329
+ _ = cfg
4330
+ }
4331
+ `);
4332
+
4333
+ const index = new ProjectIndex(tmpDir);
4334
+ index.build('**/*.go', { quiet: true });
4335
+
4336
+ // Get exporters for the config package
4337
+ const exportersResult = index.exporters(path.join(tmpDir, 'pkg', 'config', 'config.go'));
4338
+ assert.ok(exportersResult.length > 0, 'Should find files that import the config package');
4339
+
4340
+ // main.go should be in the list
4341
+ const mainFile = exportersResult.find(e => e.file.includes('main.go'));
4342
+ assert.ok(mainFile, 'main.go should import the config package');
4343
+ } finally {
4344
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4345
+ }
4346
+ });
4347
+
4348
+ it('should detect Go method calls in usages (field_identifier)', () => {
4349
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-method-'));
4350
+ try {
4351
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), `module example.com/test
4352
+ go 1.21
4353
+ `);
4354
+
4355
+ fs.writeFileSync(path.join(tmpDir, 'service.go'), `package main
4356
+
4357
+ type Service struct{}
4358
+
4359
+ func (s *Service) CollectAll() error {
4360
+ return nil
4361
+ }
4362
+
4363
+ func main() {
4364
+ svc := &Service{}
4365
+ svc.CollectAll()
4366
+ }
4367
+ `);
4368
+
4369
+ const index = new ProjectIndex(tmpDir);
4370
+ index.build('**/*.go', { quiet: true });
4371
+
4372
+ // usages should find the method call
4373
+ const usages = index.usages('CollectAll', { codeOnly: true });
4374
+ const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
4375
+ assert.ok(calls.length >= 1, 'Should find at least 1 call to CollectAll');
4376
+ } finally {
4377
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4378
+ }
4379
+ });
4380
+
4381
+ it('should find callees for Go receiver methods', () => {
4382
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-callees-'));
4383
+ try {
4384
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), `module example.com/test
4385
+ go 1.21
4386
+ `);
4387
+
4388
+ fs.writeFileSync(path.join(tmpDir, 'client.go'), `package main
4389
+
4390
+ type Client struct{}
4391
+
4392
+ func (c *Client) GetPods() []string {
4393
+ return nil
4394
+ }
4395
+
4396
+ func (c *Client) GetNodes() []string {
4397
+ return nil
4398
+ }
4399
+
4400
+ func (c *Client) CollectAll() {
4401
+ c.GetPods()
4402
+ c.GetNodes()
4403
+ }
4404
+ `);
4405
+
4406
+ const index = new ProjectIndex(tmpDir);
4407
+ index.build('**/*.go', { quiet: true });
4408
+
4409
+ // context should find callees (Go method calls)
4410
+ const ctx = index.context('CollectAll');
4411
+ assert.ok(ctx.callees, 'Should have callees');
4412
+ assert.ok(ctx.callees.length >= 2, 'Should find at least 2 callees (GetPods, GetNodes)');
4413
+
4414
+ const calleeNames = ctx.callees.map(c => c.name);
4415
+ assert.ok(calleeNames.includes('GetPods'), 'GetPods should be a callee');
4416
+ assert.ok(calleeNames.includes('GetNodes'), 'GetNodes should be a callee');
4417
+ } finally {
4418
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4419
+ }
4420
+ });
4421
+
4422
+ it('should filter by --file for Go methods with same name', () => {
4423
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-go-file-filter-'));
4424
+ try {
4425
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), `module example.com/test
4426
+ go 1.21
4427
+ `);
4428
+
4429
+ fs.writeFileSync(path.join(tmpDir, 'service_a.go'), `package main
4430
+
4431
+ type ServiceA struct{}
4432
+
4433
+ func (s *ServiceA) Process() error {
4434
+ return nil
4435
+ }
4436
+ `);
4437
+
4438
+ fs.writeFileSync(path.join(tmpDir, 'service_b.go'), `package main
4439
+
4440
+ type ServiceB struct{}
4441
+
4442
+ func (s *ServiceB) Process() error {
4443
+ return nil
4444
+ }
4445
+ `);
4446
+
4447
+ const index = new ProjectIndex(tmpDir);
4448
+ index.build('**/*.go', { quiet: true });
4449
+
4450
+ // Without file filter, should find both
4451
+ const allDefs = index.find('Process');
4452
+ assert.strictEqual(allDefs.length, 2, 'Should find 2 definitions of Process');
4453
+
4454
+ // With file filter, should find only one
4455
+ const filteredDefs = index.find('Process', { file: 'service_a.go' });
4456
+ assert.strictEqual(filteredDefs.length, 1, 'Should find 1 definition with file filter');
4457
+ assert.ok(filteredDefs[0].relativePath.includes('service_a.go'), 'Should be from service_a.go');
4458
+ } finally {
4459
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4460
+ }
4461
+ });
4300
4462
  });
4301
4463
 
4302
4464
  console.log('UCN v3 Test Suite');