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.
- package/README.md +6 -2
- package/cli/index.js +145 -43
- package/core/imports.js +153 -4
- package/core/output.js +129 -147
- package/core/project.js +365 -122
- package/languages/go.js +21 -10
- package/languages/java.js +25 -9
- package/languages/javascript.js +56 -37
- package/languages/python.js +39 -10
- package/languages/rust.js +36 -8
- package/package.json +1 -1
- package/test/parser.test.js +967 -7
- package/test/reliability-test-prompt.md +58 -0
package/test/parser.test.js
CHANGED
|
@@ -1619,15 +1619,10 @@ module.exports = { existingFunc };
|
|
|
1619
1619
|
});
|
|
1620
1620
|
});
|
|
1621
1621
|
|
|
1622
|
-
it('context should return
|
|
1622
|
+
it('context should return null for non-existent symbol', () => {
|
|
1623
1623
|
withTempProject((index) => {
|
|
1624
1624
|
const ctx = index.context('nonExistentSymbol');
|
|
1625
|
-
assert.
|
|
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');
|