ucn 3.2.0 → 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.

@@ -1619,15 +1619,10 @@ module.exports = { existingFunc };
1619
1619
  });
1620
1620
  });
1621
1621
 
1622
- it('context should return empty callers/callees for non-existent symbol', () => {
1622
+ it('context should return null for non-existent symbol', () => {
1623
1623
  withTempProject((index) => {
1624
1624
  const ctx = index.context('nonExistentSymbol');
1625
- assert.ok(ctx, 'Should return context object');
1626
- assert.strictEqual(ctx.function, 'nonExistentSymbol', 'Should include queried name');
1627
- assert.ok(Array.isArray(ctx.callers), 'Callers should be array');
1628
- assert.ok(Array.isArray(ctx.callees), 'Callees should be array');
1629
- assert.strictEqual(ctx.callers.length, 0, 'Callers should be empty');
1630
- assert.strictEqual(ctx.callees.length, 0, 'Callees should be empty');
1625
+ assert.strictEqual(ctx, null, 'Should return null for non-existent symbol');
1631
1626
  });
1632
1627
  });
1633
1628
 
@@ -4643,5 +4638,970 @@ fn main() {
4643
4638
  });
4644
4639
  });
4645
4640
 
4641
+ // ============================================================================
4642
+ // REGRESSION: Reliability fixes (2026-02)
4643
+ // ============================================================================
4644
+
4645
+ describe('Regression: Context class label bug', () => {
4646
+ it('should show class name in context, not undefined', () => {
4647
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-ctx-class-${Date.now()}`);
4648
+ fs.mkdirSync(tmpDir, { recursive: true });
4649
+
4650
+ try {
4651
+ // Python class
4652
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
4653
+ fs.writeFileSync(path.join(tmpDir, 'app.py'), `
4654
+ class Session:
4655
+ def __init__(self):
4656
+ self.data = {}
4657
+
4658
+ def get(self, key):
4659
+ return self.data.get(key)
4660
+
4661
+ def create_session():
4662
+ return Session()
4663
+ `);
4664
+
4665
+ const index = new ProjectIndex(tmpDir);
4666
+ index.build(null, { quiet: true });
4667
+
4668
+ const ctx = index.context('Session');
4669
+ assert.strictEqual(ctx.type, 'class', 'Should detect as class type');
4670
+ assert.strictEqual(ctx.name, 'Session', 'Should have class name, not undefined');
4671
+ assert.ok(ctx.name !== undefined, 'Name must not be undefined');
4672
+ } finally {
4673
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4674
+ }
4675
+ });
4676
+
4677
+ it('should show Java class name in context', () => {
4678
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-ctx-java-${Date.now()}`);
4679
+ fs.mkdirSync(tmpDir, { recursive: true });
4680
+
4681
+ try {
4682
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
4683
+ fs.writeFileSync(path.join(tmpDir, 'Gson.java'), `
4684
+ public class Gson {
4685
+ public Gson() {}
4686
+ public String toJson(Object src) {
4687
+ return "";
4688
+ }
4689
+ }
4690
+ `);
4691
+
4692
+ const index = new ProjectIndex(tmpDir);
4693
+ index.build(null, { quiet: true });
4694
+
4695
+ const ctx = index.context('Gson');
4696
+ assert.strictEqual(ctx.type, 'class');
4697
+ assert.strictEqual(ctx.name, 'Gson', 'Should show Gson, not undefined');
4698
+ } finally {
4699
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4700
+ }
4701
+ });
4702
+ });
4703
+
4704
+ describe('Regression: Java duplicate constructor entries', () => {
4705
+ it('should not duplicate constructors in find results', () => {
4706
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-java-dedup-${Date.now()}`);
4707
+ fs.mkdirSync(tmpDir, { recursive: true });
4708
+
4709
+ try {
4710
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
4711
+ fs.writeFileSync(path.join(tmpDir, 'MyClass.java'), `
4712
+ public class MyClass {
4713
+ private int value;
4714
+
4715
+ public MyClass() {
4716
+ this.value = 0;
4717
+ }
4718
+
4719
+ public MyClass(int value) {
4720
+ this.value = value;
4721
+ }
4722
+
4723
+ public int getValue() {
4724
+ return value;
4725
+ }
4726
+ }
4727
+ `);
4728
+
4729
+ const index = new ProjectIndex(tmpDir);
4730
+ index.build(null, { quiet: true });
4731
+
4732
+ const symbols = index.symbols.get('MyClass') || [];
4733
+ // Should have: 1 class + 2 constructors (as members) = 3 entries
4734
+ // Should NOT have: extra duplicates from findFunctions
4735
+ const types = symbols.map(s => s.type);
4736
+ assert.strictEqual(types.filter(t => t === 'class').length, 1, 'Should have exactly 1 class entry');
4737
+ // Constructors should only come from extractClassMembers, not findFunctions
4738
+ const constructors = symbols.filter(s => s.type === 'constructor');
4739
+ assert.strictEqual(constructors.length, 2, 'Should have exactly 2 constructor entries');
4740
+ // Each constructor at a unique line
4741
+ const lines = constructors.map(c => c.startLine);
4742
+ assert.notStrictEqual(lines[0], lines[1], 'Constructors should be at different lines');
4743
+ } finally {
4744
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4745
+ }
4746
+ });
4747
+ });
4748
+
4749
+ describe('Regression: Java overloaded method callees', () => {
4750
+ it('should detect callees for overloaded methods', () => {
4751
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-java-overload-${Date.now()}`);
4752
+ fs.mkdirSync(tmpDir, { recursive: true });
4753
+
4754
+ try {
4755
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
4756
+ fs.writeFileSync(path.join(tmpDir, 'Converter.java'), `
4757
+ public class Converter {
4758
+ public String convert(Object src) {
4759
+ return convert(src, src.getClass());
4760
+ }
4761
+
4762
+ public String convert(Object src, Class<?> type) {
4763
+ return type.getName() + ": " + src.toString();
4764
+ }
4765
+ }
4766
+ `);
4767
+
4768
+ const index = new ProjectIndex(tmpDir);
4769
+ index.build(null, { quiet: true });
4770
+
4771
+ // The first overload calls the second — smart should show it as a dependency
4772
+ const smart = index.smart('convert', { file: 'Converter' });
4773
+ assert.ok(smart, 'smart should return a result');
4774
+ // Should have at least 1 dependency (the other overload)
4775
+ assert.ok(smart.dependencies.length >= 1,
4776
+ `Should find overload as dependency, got ${smart.dependencies.length}`);
4777
+ } finally {
4778
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4779
+ }
4780
+ });
4781
+ });
4782
+
4783
+ describe('Regression: Disambiguation prefers non-test definitions', () => {
4784
+ it('should prefer src definition over test definition', () => {
4785
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-disambig-${Date.now()}`);
4786
+ fs.mkdirSync(tmpDir, { recursive: true });
4787
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
4788
+ fs.mkdirSync(path.join(tmpDir, 'test'), { recursive: true });
4789
+
4790
+ try {
4791
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}');
4792
+ fs.writeFileSync(path.join(tmpDir, 'src', 'render.js'), `
4793
+ function render(template, data) {
4794
+ return template.replace(/{(\\w+)}/g, (_, key) => data[key] || '');
4795
+ }
4796
+ module.exports = { render };
4797
+ `);
4798
+ fs.writeFileSync(path.join(tmpDir, 'test', 'render.test.js'), `
4799
+ const { render } = require('../src/render');
4800
+ function render(mockTemplate) {
4801
+ return 'mock: ' + mockTemplate;
4802
+ }
4803
+ test('render', () => { render('hello'); });
4804
+ `);
4805
+
4806
+ const index = new ProjectIndex(tmpDir);
4807
+ index.build(null, { quiet: true });
4808
+
4809
+ // resolveSymbol should prefer src/render.js over test/render.test.js
4810
+ const { def } = index.resolveSymbol('render');
4811
+ assert.ok(def, 'Should find render');
4812
+ assert.ok(!def.relativePath.includes('test'),
4813
+ `Should prefer non-test file, got ${def.relativePath}`);
4814
+ } finally {
4815
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4816
+ }
4817
+ });
4818
+
4819
+ it('should use consistent selection across context, smart, trace', () => {
4820
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-consistent-${Date.now()}`);
4821
+ fs.mkdirSync(tmpDir, { recursive: true });
4822
+
4823
+ try {
4824
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}');
4825
+ fs.writeFileSync(path.join(tmpDir, 'a.js'), `
4826
+ function process(data) {
4827
+ return transform(data);
4828
+ }
4829
+ function transform(x) { return x * 2; }
4830
+ module.exports = { process };
4831
+ `);
4832
+ fs.writeFileSync(path.join(tmpDir, 'b.js'), `
4833
+ function process(item) {
4834
+ return item.toString();
4835
+ }
4836
+ module.exports = { process };
4837
+ `);
4838
+
4839
+ const index = new ProjectIndex(tmpDir);
4840
+ index.build(null, { quiet: true });
4841
+
4842
+ const ctx = index.context('process');
4843
+ const smart = index.smart('process');
4844
+ const trace = index.trace('process');
4845
+
4846
+ // All should pick the same definition
4847
+ assert.strictEqual(ctx.file, smart.target.relativePath,
4848
+ 'context and smart should pick same definition');
4849
+ assert.strictEqual(ctx.file, trace.file,
4850
+ 'context and trace should pick same definition');
4851
+ } finally {
4852
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4853
+ }
4854
+ });
4855
+ });
4856
+
4857
+ describe('Regression: Verify filters method calls', () => {
4858
+ it('should not count obj.get() as call to standalone get()', () => {
4859
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-verify-${Date.now()}`);
4860
+ fs.mkdirSync(tmpDir, { recursive: true });
4861
+
4862
+ try {
4863
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
4864
+ fs.writeFileSync(path.join(tmpDir, 'api.py'), `
4865
+ def get(url, params=None):
4866
+ return request("GET", url, params=params)
4867
+
4868
+ def request(method, url, params=None):
4869
+ pass
4870
+ `);
4871
+ fs.writeFileSync(path.join(tmpDir, 'client.py'), `
4872
+ from .api import get
4873
+
4874
+ def fetch_data():
4875
+ result = get("/api/data")
4876
+ headers = {"Host": "example.com"}
4877
+ host = headers.get("Host")
4878
+ data = {"key": "value"}
4879
+ val = data.get("key")
4880
+ return result
4881
+ `);
4882
+
4883
+ const index = new ProjectIndex(tmpDir);
4884
+ index.build(null, { quiet: true });
4885
+
4886
+ const result = index.verify('get');
4887
+ assert.ok(result.found, 'Should find get function');
4888
+ // Should NOT count headers.get("Host") or data.get("key") as mismatches
4889
+ // Only get("/api/data") should be counted
4890
+ assert.strictEqual(result.mismatches, 0,
4891
+ `Should have 0 mismatches (method calls filtered), got ${result.mismatches}`);
4892
+ } finally {
4893
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4894
+ }
4895
+ });
4896
+ });
4897
+
4898
+ describe('Regression: Python relative import resolution', () => {
4899
+ it('should resolve from .module import in exporters', () => {
4900
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-pyimport-${Date.now()}`);
4901
+ const pkgDir = path.join(tmpDir, 'mypackage');
4902
+ fs.mkdirSync(pkgDir, { recursive: true });
4903
+
4904
+ try {
4905
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
4906
+ fs.writeFileSync(path.join(pkgDir, '__init__.py'), `
4907
+ from .models import User, Product
4908
+ from .utils import helper
4909
+ `);
4910
+ fs.writeFileSync(path.join(pkgDir, 'models.py'), `
4911
+ class User:
4912
+ def __init__(self, name):
4913
+ self.name = name
4914
+
4915
+ class Product:
4916
+ def __init__(self, title):
4917
+ self.title = title
4918
+ `);
4919
+ fs.writeFileSync(path.join(pkgDir, 'utils.py'), `
4920
+ def helper():
4921
+ return "help"
4922
+ `);
4923
+
4924
+ const index = new ProjectIndex(tmpDir);
4925
+ index.build(null, { quiet: true });
4926
+
4927
+ // models.py should be found as an exporter (imported by __init__.py)
4928
+ const modelsPath = path.join(pkgDir, 'models.py');
4929
+ const exporters = index.exportGraph.get(modelsPath) || [];
4930
+ assert.ok(exporters.length > 0,
4931
+ `models.py should have importers, got ${exporters.length}`);
4932
+
4933
+ // __init__.py should import models.py
4934
+ const initPath = path.join(pkgDir, '__init__.py');
4935
+ const imports = index.importGraph.get(initPath) || [];
4936
+ assert.ok(imports.some(i => i.includes('models')),
4937
+ `__init__.py should import models.py, got: ${imports.map(i => path.basename(i))}`);
4938
+ } finally {
4939
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4940
+ }
4941
+ });
4942
+
4943
+ it('should resolve parent relative imports (from ..utils import)', () => {
4944
+ const { resolveImport } = require('../core/imports');
4945
+
4946
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-pyrel-${Date.now()}`);
4947
+ const subDir = path.join(tmpDir, 'pkg', 'sub');
4948
+ fs.mkdirSync(subDir, { recursive: true });
4949
+
4950
+ try {
4951
+ fs.writeFileSync(path.join(tmpDir, 'pkg', 'utils.py'), 'def helper(): pass');
4952
+ fs.writeFileSync(path.join(subDir, 'mod.py'), 'from ..utils import helper');
4953
+
4954
+ const resolved = resolveImport('..utils', path.join(subDir, 'mod.py'), {
4955
+ language: 'python',
4956
+ root: tmpDir
4957
+ });
4958
+ assert.ok(resolved, 'Should resolve ..utils');
4959
+ assert.ok(resolved.endsWith('utils.py'), `Should resolve to utils.py, got ${resolved}`);
4960
+ } finally {
4961
+ fs.rmSync(tmpDir, { recursive: true, force: true });
4962
+ }
4963
+ });
4964
+ });
4965
+
4966
+ describe('Regression: Java inner classes found after constructor dedup', () => {
4967
+ it('should find inner classes with their own members', () => {
4968
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-inner-${Date.now()}`);
4969
+ fs.mkdirSync(tmpDir, { recursive: true });
4970
+
4971
+ try {
4972
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
4973
+ fs.writeFileSync(path.join(tmpDir, 'Outer.java'), `
4974
+ public class Outer {
4975
+ public static class Inner {
4976
+ private int x;
4977
+
4978
+ public Inner(int x) {
4979
+ this.x = x;
4980
+ }
4981
+
4982
+ public int getX() {
4983
+ return x;
4984
+ }
4985
+ }
4986
+
4987
+ public Inner create() {
4988
+ return new Inner(42);
4989
+ }
4990
+ }
4991
+ `);
4992
+
4993
+ const index = new ProjectIndex(tmpDir);
4994
+ index.build(null, { quiet: true });
4995
+
4996
+ // Inner class should be found
4997
+ assert.ok(index.symbols.has('Inner'), 'Should find Inner class');
4998
+ const innerSyms = index.symbols.get('Inner');
4999
+ const innerClass = innerSyms.find(s => s.type === 'class');
5000
+ assert.ok(innerClass, 'Should have Inner as class type');
5001
+
5002
+ // Outer class should also be found
5003
+ assert.ok(index.symbols.has('Outer'), 'Should find Outer class');
5004
+ } finally {
5005
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5006
+ }
5007
+ });
5008
+ });
5009
+
5010
+ describe('Regression: fn command auto-resolves best definition', () => {
5011
+ it('should prefer src/lib definition over test definition via pickBestDefinition-style scoring', () => {
5012
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-fn-resolve-${Date.now()}`);
5013
+ fs.mkdirSync(path.join(tmpDir, 'lib'), { recursive: true });
5014
+ fs.mkdirSync(path.join(tmpDir, 'test'), { recursive: true });
5015
+
5016
+ try {
5017
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}');
5018
+ fs.writeFileSync(path.join(tmpDir, 'lib', 'app.js'), `
5019
+ function render(template, data) {
5020
+ return template.replace(/{(\\w+)}/g, (_, key) => data[key] || '');
5021
+ }
5022
+ module.exports = { render };
5023
+ `);
5024
+ fs.writeFileSync(path.join(tmpDir, 'test', 'app.test.js'), `
5025
+ const { render } = require('../lib/app');
5026
+ function render(mockTemplate) {
5027
+ return 'mock: ' + mockTemplate;
5028
+ }
5029
+ test('render works', () => { render('hello'); });
5030
+ `);
5031
+
5032
+ const index = new ProjectIndex(tmpDir);
5033
+ index.build(null, { quiet: true });
5034
+
5035
+ // find should return both definitions
5036
+ const matches = index.find('render').filter(m => m.type === 'function' || m.params !== undefined);
5037
+ assert.ok(matches.length >= 2, `Should find at least 2 definitions, got ${matches.length}`);
5038
+
5039
+ // resolveSymbol should prefer lib/ over test/
5040
+ const { def } = index.resolveSymbol('render');
5041
+ assert.ok(def, 'Should find render');
5042
+ assert.ok(def.relativePath.includes('lib/'),
5043
+ `Should prefer lib/ file, got ${def.relativePath}`);
5044
+ } finally {
5045
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5046
+ }
5047
+ });
5048
+ });
5049
+
5050
+ describe('Regression: Java package import resolution for exporters', () => {
5051
+ it('should resolve Java package imports and find exporters', () => {
5052
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-java-exports-${Date.now()}`);
5053
+ const pkgDir = path.join(tmpDir, 'src', 'main', 'java', 'com', 'example');
5054
+ fs.mkdirSync(pkgDir, { recursive: true });
5055
+
5056
+ try {
5057
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
5058
+ fs.writeFileSync(path.join(pkgDir, 'Model.java'), `
5059
+ package com.example;
5060
+ public class Model {
5061
+ private String name;
5062
+ public String getName() { return name; }
5063
+ }
5064
+ `);
5065
+ fs.writeFileSync(path.join(pkgDir, 'Service.java'), `
5066
+ package com.example;
5067
+ import com.example.Model;
5068
+ public class Service {
5069
+ public Model getModel() { return new Model(); }
5070
+ }
5071
+ `);
5072
+ fs.writeFileSync(path.join(pkgDir, 'Controller.java'), `
5073
+ package com.example;
5074
+ import com.example.Model;
5075
+ import com.example.Service;
5076
+ public class Controller {
5077
+ private Service service = new Service();
5078
+ public Model handle() { return service.getModel(); }
5079
+ }
5080
+ `);
5081
+
5082
+ const index = new ProjectIndex(tmpDir);
5083
+ index.build(null, { quiet: true });
5084
+
5085
+ // Model.java should have exporters (Service.java and Controller.java import it)
5086
+ const modelExporters = index.exporters('src/main/java/com/example/Model.java');
5087
+ assert.ok(modelExporters.length >= 2,
5088
+ `Model.java should have at least 2 importers, got ${modelExporters.length}: ${JSON.stringify(modelExporters.map(e => e.file))}`);
5089
+
5090
+ // Service.java should also have an exporter (Controller.java imports it)
5091
+ const serviceExporters = index.exporters('src/main/java/com/example/Service.java');
5092
+ assert.ok(serviceExporters.length >= 1,
5093
+ `Service.java should have at least 1 importer, got ${serviceExporters.length}`);
5094
+ } finally {
5095
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5096
+ }
5097
+ });
5098
+ });
5099
+
5100
+ describe('Regression: Java overload callees finds ALL overloads', () => {
5101
+ it('should find all overload callees, not just the first', () => {
5102
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-java-all-overloads-${Date.now()}`);
5103
+ fs.mkdirSync(tmpDir, { recursive: true });
5104
+
5105
+ try {
5106
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
5107
+ fs.writeFileSync(path.join(tmpDir, 'Serializer.java'), `
5108
+ public class Serializer {
5109
+ public String serialize(Object src) {
5110
+ if (src == null) {
5111
+ return serialize("null_value");
5112
+ }
5113
+ return serialize(src, src.getClass());
5114
+ }
5115
+
5116
+ public String serialize(Object src, Class<?> type) {
5117
+ return type.getName() + ": " + src.toString();
5118
+ }
5119
+
5120
+ public String serialize(String value) {
5121
+ return "string: " + value;
5122
+ }
5123
+ }
5124
+ `);
5125
+
5126
+ const index = new ProjectIndex(tmpDir);
5127
+ index.build(null, { quiet: true });
5128
+
5129
+ // smart for the first overload should show other overloads as dependencies
5130
+ const smart = index.smart('serialize', { file: 'Serializer' });
5131
+ assert.ok(smart, 'smart should return a result');
5132
+
5133
+ // Should find at least 2 overload dependencies (the other two overloads)
5134
+ assert.ok(smart.dependencies.length >= 2,
5135
+ `Should find at least 2 overload dependencies, got ${smart.dependencies.length}: ${smart.dependencies.map(d => d.startLine).join(', ')}`);
5136
+
5137
+ // Each dependency should be a different overload (different startLine)
5138
+ const depLines = new Set(smart.dependencies.map(d => d.startLine));
5139
+ assert.ok(depLines.size >= 2,
5140
+ `Dependencies should be distinct overloads, got ${depLines.size} unique`);
5141
+ } finally {
5142
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5143
+ }
5144
+ });
5145
+
5146
+ it('should use binding ID for exact symbol lookup', () => {
5147
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-java-binding-${Date.now()}`);
5148
+ fs.mkdirSync(tmpDir, { recursive: true });
5149
+
5150
+ try {
5151
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
5152
+ fs.writeFileSync(path.join(tmpDir, 'Builder.java'), `
5153
+ public class Builder {
5154
+ public Builder set(String key, Object value) {
5155
+ return set(key, value, false);
5156
+ }
5157
+
5158
+ public Builder set(String key, Object value, boolean override) {
5159
+ return this;
5160
+ }
5161
+
5162
+ public String build() {
5163
+ return "built";
5164
+ }
5165
+ }
5166
+ `);
5167
+
5168
+ const index = new ProjectIndex(tmpDir);
5169
+ index.build(null, { quiet: true });
5170
+
5171
+ // context for the first set() should show the second set() as a callee
5172
+ const ctx = index.context('set', { file: 'Builder' });
5173
+ assert.ok(ctx, 'context should return a result');
5174
+ assert.ok(ctx.callees.length >= 1,
5175
+ `Should find at least 1 callee (the other overload), got ${ctx.callees.length}`);
5176
+ } finally {
5177
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5178
+ }
5179
+ });
5180
+ });
5181
+
5182
+ // ============================================================================
5183
+ // Regression: fn command extracts class methods (not just top-level functions)
5184
+ // ============================================================================
5185
+ describe('Regression: fn command extracts class methods', () => {
5186
+ it('should find and extract Python __init__ method via symbol index', () => {
5187
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-fn-method-${Date.now()}`);
5188
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
5189
+
5190
+ try {
5191
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
5192
+ fs.writeFileSync(path.join(tmpDir, 'src', 'models.py'), `
5193
+ class Session:
5194
+ def __init__(self, url, timeout=30):
5195
+ self.url = url
5196
+ self.timeout = timeout
5197
+
5198
+ def get(self, path):
5199
+ return self.url + path
5200
+ `);
5201
+
5202
+ const index = new ProjectIndex(tmpDir);
5203
+ index.build(null, { quiet: true });
5204
+
5205
+ // find should return __init__ (it has params, so it passes the filter)
5206
+ const matches = index.find('__init__').filter(m => m.type === 'function' || m.params !== undefined);
5207
+ assert.ok(matches.length >= 1, `Should find __init__, got ${matches.length}`);
5208
+
5209
+ // The match should have valid startLine/endLine for direct code extraction
5210
+ const match = matches[0];
5211
+ assert.ok(match.startLine, 'Match should have startLine');
5212
+ assert.ok(match.endLine, 'Match should have endLine');
5213
+ assert.ok(match.file, 'Match should have file path');
5214
+
5215
+ // Extract code using startLine/endLine (same approach as the fixed fn command)
5216
+ const code = fs.readFileSync(match.file, 'utf-8');
5217
+ const lines = code.split('\n');
5218
+ const fnCode = lines.slice(match.startLine - 1, match.endLine).join('\n');
5219
+ assert.ok(fnCode.includes('def __init__'), `Extracted code should contain __init__, got: ${fnCode}`);
5220
+ assert.ok(fnCode.includes('self.url = url'), `Extracted code should contain method body, got: ${fnCode}`);
5221
+ } finally {
5222
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5223
+ }
5224
+ });
5225
+
5226
+ it('should find and extract Java overloaded method via symbol index', () => {
5227
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-fn-overload-${Date.now()}`);
5228
+ const pkgDir = path.join(tmpDir, 'src', 'main', 'java', 'com', 'example');
5229
+ fs.mkdirSync(pkgDir, { recursive: true });
5230
+
5231
+ try {
5232
+ fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
5233
+ fs.writeFileSync(path.join(pkgDir, 'Converter.java'), `
5234
+ package com.example;
5235
+ public class Converter {
5236
+ public String toJson(Object obj) {
5237
+ return obj.toString();
5238
+ }
5239
+ public String toJson(Object obj, boolean pretty) {
5240
+ String result = obj.toString();
5241
+ return pretty ? format(result) : result;
5242
+ }
5243
+ private String format(String s) {
5244
+ return s;
5245
+ }
5246
+ }
5247
+ `);
5248
+
5249
+ const index = new ProjectIndex(tmpDir);
5250
+ index.build(null, { quiet: true });
5251
+
5252
+ // find should return toJson overloads
5253
+ const matches = index.find('toJson').filter(m => m.type === 'function' || m.params !== undefined);
5254
+ assert.ok(matches.length >= 1, `Should find toJson, got ${matches.length}`);
5255
+
5256
+ // Each match should have valid location for direct extraction
5257
+ for (const match of matches) {
5258
+ assert.ok(match.startLine, `Match at ${match.relativePath} should have startLine`);
5259
+ assert.ok(match.endLine, `Match at ${match.relativePath} should have endLine`);
5260
+
5261
+ const code = fs.readFileSync(match.file, 'utf-8');
5262
+ const lines = code.split('\n');
5263
+ const fnCode = lines.slice(match.startLine - 1, match.endLine).join('\n');
5264
+ assert.ok(fnCode.includes('toJson'), `Extracted code should contain toJson, got: ${fnCode}`);
5265
+ }
5266
+ } finally {
5267
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5268
+ }
5269
+ });
5270
+ });
5271
+
5272
+ // ============================================================================
5273
+ // Regression: verify totalCalls excludes filtered method calls
5274
+ // ============================================================================
5275
+ describe('Regression: verify totalCalls excludes filtered method calls', () => {
5276
+ it('should not count method calls in totalCalls for standalone function', () => {
5277
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-verify-total-${Date.now()}`);
5278
+ fs.mkdirSync(tmpDir, { recursive: true });
5279
+
5280
+ try {
5281
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
5282
+ fs.writeFileSync(path.join(tmpDir, 'api.py'), `
5283
+ def get(url, params=None):
5284
+ return request("GET", url, params=params)
5285
+
5286
+ def request(method, url, params=None):
5287
+ pass
5288
+ `);
5289
+ fs.writeFileSync(path.join(tmpDir, 'client.py'), `
5290
+ from .api import get
5291
+
5292
+ def fetch_data():
5293
+ result = get("/api/data")
5294
+ headers = {"Host": "example.com"}
5295
+ host = headers.get("Host")
5296
+ data = {"key": "value"}
5297
+ val = data.get("key")
5298
+ return result
5299
+ `);
5300
+
5301
+ const index = new ProjectIndex(tmpDir);
5302
+ index.build(null, { quiet: true });
5303
+
5304
+ const result = index.verify('get');
5305
+ assert.ok(result.found, 'Should find get function');
5306
+ // totalCalls should equal valid + mismatches + uncertain (no inflated count)
5307
+ assert.strictEqual(result.totalCalls, result.valid + result.mismatches + result.uncertain,
5308
+ `totalCalls (${result.totalCalls}) should equal valid (${result.valid}) + mismatches (${result.mismatches}) + uncertain (${result.uncertain})`);
5309
+ // Specifically, method calls like headers.get() and data.get() should NOT be in totalCalls
5310
+ assert.ok(result.totalCalls <= 2,
5311
+ `totalCalls should be at most 2 (direct calls only), got ${result.totalCalls}`);
5312
+ } finally {
5313
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5314
+ }
5315
+ });
5316
+
5317
+ it('should have consistent totals for Go method calls', () => {
5318
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-verify-go-${Date.now()}`);
5319
+ fs.mkdirSync(tmpDir, { recursive: true });
5320
+
5321
+ try {
5322
+ fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/test\n\ngo 1.21');
5323
+ fs.writeFileSync(path.join(tmpDir, 'main.go'), `
5324
+ package main
5325
+
5326
+ import "os/exec"
5327
+
5328
+ func Run(opts string) error {
5329
+ return nil
5330
+ }
5331
+
5332
+ func main() {
5333
+ Run("hello")
5334
+ cmd := exec.Command("ls")
5335
+ cmd.Run()
5336
+ }
5337
+ `);
5338
+
5339
+ const index = new ProjectIndex(tmpDir);
5340
+ index.build(null, { quiet: true });
5341
+
5342
+ const result = index.verify('Run');
5343
+ assert.ok(result.found, 'Should find Run function');
5344
+ // totalCalls must always equal valid + mismatches + uncertain
5345
+ assert.strictEqual(result.totalCalls, result.valid + result.mismatches + result.uncertain,
5346
+ `totalCalls (${result.totalCalls}) should equal valid (${result.valid}) + mismatches (${result.mismatches}) + uncertain (${result.uncertain})`);
5347
+ // cmd.Run() is a method call and should NOT inflate totalCalls
5348
+ assert.ok(result.totalCalls >= 1,
5349
+ `Should find at least 1 direct call to Run, got ${result.totalCalls}`);
5350
+ } finally {
5351
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5352
+ }
5353
+ });
5354
+ });
5355
+
5356
+ // ============================================================================
5357
+ // Regression: context returns null for non-existent symbols
5358
+ // ============================================================================
5359
+ describe('Regression: context returns null for non-existent symbols', () => {
5360
+ it('should return null when symbol is not defined in the project', () => {
5361
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-context-null-${Date.now()}`);
5362
+ fs.mkdirSync(tmpDir, { recursive: true });
5363
+
5364
+ try {
5365
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"test"}');
5366
+ fs.writeFileSync(path.join(tmpDir, 'app.js'), `
5367
+ const router = require('express').Router();
5368
+ function handleRequest(req, res) {
5369
+ res.send('hello');
5370
+ }
5371
+ module.exports = { handleRequest };
5372
+ `);
5373
+
5374
+ const index = new ProjectIndex(tmpDir);
5375
+ index.build(null, { quiet: true });
5376
+
5377
+ // Symbol that doesn't exist at all
5378
+ const result1 = index.context('nonexistentXYZ');
5379
+ assert.strictEqual(result1, null,
5380
+ 'context should return null for completely non-existent symbol');
5381
+
5382
+ // Symbol used but not defined in project (external import)
5383
+ const result2 = index.context('Router');
5384
+ assert.strictEqual(result2, null,
5385
+ 'context should return null for externally-defined symbol');
5386
+
5387
+ // Symbol that IS defined should still work
5388
+ const result3 = index.context('handleRequest');
5389
+ assert.ok(result3, 'context should return result for defined symbol');
5390
+ assert.ok(result3.function || result3.name, 'Result should have function/name field');
5391
+ } finally {
5392
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5393
+ }
5394
+ });
5395
+ });
5396
+
5397
+ // Regression: pickBestDefinition prefers larger function bodies as tiebreaker
5398
+ describe('Regression: pickBestDefinition prefers larger functions over trivial ones', () => {
5399
+ it('should pick the __init__ with the largest body when all else is equal', () => {
5400
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-pick-best-${Date.now()}`);
5401
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
5402
+
5403
+ try {
5404
+ fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '[project]\nname = "test"');
5405
+ // Small __init__ (3 lines) - should NOT be preferred
5406
+ fs.writeFileSync(path.join(tmpDir, 'src', 'errors.py'), `
5407
+ class AppError(Exception):
5408
+ def __init__(self, message):
5409
+ super().__init__(message)
5410
+ self.message = message
5411
+ `);
5412
+ // Large __init__ (20+ lines) - SHOULD be preferred
5413
+ fs.writeFileSync(path.join(tmpDir, 'src', 'client.py'), `
5414
+ class Client:
5415
+ def __init__(self, url, timeout=30, retries=3, auth=None, headers=None):
5416
+ self.url = url
5417
+ self.timeout = timeout
5418
+ self.retries = retries
5419
+ self.auth = auth
5420
+ self.headers = headers or {}
5421
+ self.session = None
5422
+ self._pool = None
5423
+ self._closed = False
5424
+ self._setup_logging()
5425
+ self._verify_ssl = True
5426
+ self._proxy = None
5427
+ self._max_redirects = 10
5428
+ self._cookies = {}
5429
+ self._default_encoding = 'utf-8'
5430
+ self._event_hooks = {'request': [], 'response': []}
5431
+ self._transport = None
5432
+ self._base_url = url
5433
+ self._initialized = True
5434
+
5435
+ def get(self, path):
5436
+ pass
5437
+ `);
5438
+
5439
+ const index = new ProjectIndex(tmpDir);
5440
+ index.build(null, { quiet: true });
5441
+
5442
+ const matches = index.find('__init__').filter(m => m.type === 'function' || m.params !== undefined);
5443
+ assert.ok(matches.length >= 2, `Should find at least 2 __init__ methods, got ${matches.length}`);
5444
+
5445
+ // Sort using same logic as pickBestDefinition
5446
+ const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
5447
+ const scored = matches.map(m => {
5448
+ let score = 0;
5449
+ const rp = m.relativePath || '';
5450
+ if (typeOrder.has(m.type)) score += 1000;
5451
+ if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
5452
+ if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
5453
+ if (m.startLine && m.endLine) {
5454
+ score += Math.min(m.endLine - m.startLine, 100);
5455
+ }
5456
+ return { match: m, score };
5457
+ });
5458
+ scored.sort((a, b) => b.score - a.score);
5459
+ const best = scored[0].match;
5460
+
5461
+ // Should pick client.py (large body) over errors.py (small body)
5462
+ assert.ok(best.file.includes('client.py'),
5463
+ `Should prefer client.py __init__ (large body), got ${best.file}`);
5464
+ assert.ok(best.endLine - best.startLine > 10,
5465
+ `Selected __init__ should have >10 lines, got ${best.endLine - best.startLine}`);
5466
+ } finally {
5467
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5468
+ }
5469
+ });
5470
+ });
5471
+
5472
+ // Regression: Rust crate:: import resolution for exporters
5473
+ describe('Regression: Rust crate:: import resolution', () => {
5474
+ it('should resolve crate:: paths and mod declarations to file paths', () => {
5475
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-rust-imports-${Date.now()}`);
5476
+ const srcDir = path.join(tmpDir, 'src');
5477
+ const displayDir = path.join(srcDir, 'display');
5478
+ fs.mkdirSync(displayDir, { recursive: true });
5479
+
5480
+ try {
5481
+ fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test-crate"');
5482
+
5483
+ // main.rs with mod declarations
5484
+ fs.writeFileSync(path.join(srcDir, 'main.rs'), `
5485
+ mod display;
5486
+ mod config;
5487
+
5488
+ use crate::display::Display;
5489
+ use crate::config::Settings;
5490
+
5491
+ fn main() {
5492
+ let display = Display::new();
5493
+ let config = Settings::default();
5494
+ }
5495
+ `);
5496
+ // display/mod.rs
5497
+ fs.writeFileSync(path.join(displayDir, 'mod.rs'), `
5498
+ use crate::config::Settings;
5499
+
5500
+ pub struct Display {
5501
+ width: u32,
5502
+ height: u32,
5503
+ }
5504
+
5505
+ impl Display {
5506
+ pub fn new() -> Self {
5507
+ Display { width: 800, height: 600 }
5508
+ }
5509
+ }
5510
+ `);
5511
+ // config.rs
5512
+ fs.writeFileSync(path.join(srcDir, 'config.rs'), `
5513
+ pub struct Settings {
5514
+ pub theme: String,
5515
+ }
5516
+
5517
+ impl Settings {
5518
+ pub fn default() -> Self {
5519
+ Settings { theme: String::from("dark") }
5520
+ }
5521
+ }
5522
+ `);
5523
+
5524
+ const index = new ProjectIndex(tmpDir);
5525
+ index.build(null, { quiet: true });
5526
+
5527
+ // Test mod declaration resolution: main.rs imports display/ and config.rs
5528
+ const mainImporters = index.importGraph.get(path.join(srcDir, 'main.rs')) || [];
5529
+ assert.ok(mainImporters.length >= 2,
5530
+ `main.rs should import at least 2 files (display + config), got ${mainImporters.length}`);
5531
+
5532
+ // Test exporters: display/mod.rs should be imported by main.rs
5533
+ const displayExporters = index.exporters('src/display/mod.rs');
5534
+ assert.ok(displayExporters.length >= 1,
5535
+ `display/mod.rs should have at least 1 exporter, got ${displayExporters.length}`);
5536
+ assert.ok(displayExporters.some(e => e.file.includes('main.rs')),
5537
+ `main.rs should import display/mod.rs`);
5538
+
5539
+ // Test crate:: resolution: display/mod.rs imports config.rs via crate::config
5540
+ const displayImports = index.importGraph.get(path.join(displayDir, 'mod.rs')) || [];
5541
+ assert.ok(displayImports.some(i => i.includes('config.rs')),
5542
+ `display/mod.rs should import config.rs via crate::config, got ${displayImports.map(i => path.basename(i))}`);
5543
+
5544
+ // Test exporters for config.rs: should be imported by both main.rs and display/mod.rs
5545
+ const configExporters = index.exporters('src/config.rs');
5546
+ assert.ok(configExporters.length >= 2,
5547
+ `config.rs should have at least 2 exporters (main + display), got ${configExporters.length}`);
5548
+ } finally {
5549
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5550
+ }
5551
+ });
5552
+
5553
+ it('should resolve nested crate:: paths like crate::display::color', () => {
5554
+ const tmpDir = path.join(os.tmpdir(), `ucn-test-rust-nested-${Date.now()}`);
5555
+ const srcDir = path.join(tmpDir, 'src');
5556
+ const displayDir = path.join(srcDir, 'display');
5557
+ fs.mkdirSync(displayDir, { recursive: true });
5558
+
5559
+ try {
5560
+ fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '[package]\nname = "test"');
5561
+
5562
+ fs.writeFileSync(path.join(srcDir, 'main.rs'), `
5563
+ mod display;
5564
+ use crate::display::color::Rgb;
5565
+
5566
+ fn main() {
5567
+ let c = Rgb::new(255, 0, 0);
5568
+ }
5569
+ `);
5570
+ fs.writeFileSync(path.join(displayDir, 'mod.rs'), `
5571
+ pub mod color;
5572
+ pub struct Display;
5573
+ `);
5574
+ fs.writeFileSync(path.join(displayDir, 'color.rs'), `
5575
+ pub struct Rgb {
5576
+ pub r: u8,
5577
+ pub g: u8,
5578
+ pub b: u8,
5579
+ }
5580
+
5581
+ impl Rgb {
5582
+ pub fn new(r: u8, g: u8, b: u8) -> Self {
5583
+ Rgb { r, g, b }
5584
+ }
5585
+ }
5586
+ `);
5587
+
5588
+ const index = new ProjectIndex(tmpDir);
5589
+ index.build(null, { quiet: true });
5590
+
5591
+ // main.rs should resolve crate::display::color::Rgb to display/color.rs
5592
+ const mainImports = index.importGraph.get(path.join(srcDir, 'main.rs')) || [];
5593
+ assert.ok(mainImports.some(i => i.includes('color.rs')),
5594
+ `main.rs should import display/color.rs via crate::display::color::Rgb, got ${mainImports.map(i => path.basename(i))}`);
5595
+
5596
+ // color.rs exporters should include main.rs
5597
+ const colorExporters = index.exporters('src/display/color.rs');
5598
+ assert.ok(colorExporters.some(e => e.file.includes('main.rs')),
5599
+ `color.rs should be exported to main.rs`);
5600
+ } finally {
5601
+ fs.rmSync(tmpDir, { recursive: true, force: true });
5602
+ }
5603
+ });
5604
+ });
5605
+
4646
5606
  console.log('UCN v3 Test Suite');
4647
5607
  console.log('Run with: node --test test/parser.test.js');