ucn 3.3.0 → 3.4.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.
- package/cli/index.js +1 -1
- package/core/imports.js +9 -0
- package/core/project.js +47 -11
- package/languages/java.js +4 -1
- package/package.json +1 -1
- package/test/parser.test.js +250 -0
package/cli/index.js
CHANGED
|
@@ -806,7 +806,7 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
806
806
|
|
|
807
807
|
case 'verify': {
|
|
808
808
|
requireArg(arg, 'Usage: ucn . verify <name>');
|
|
809
|
-
const verifyResult = index.verify(arg);
|
|
809
|
+
const verifyResult = index.verify(arg, { file: flags.file });
|
|
810
810
|
printOutput(verifyResult, r => JSON.stringify(r, null, 2), output.formatVerify);
|
|
811
811
|
break;
|
|
812
812
|
}
|
package/core/imports.js
CHANGED
|
@@ -122,6 +122,15 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
122
122
|
if (resolved) return resolved;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// Python: non-relative package imports (e.g., "tools.analyzer" -> "tools/analyzer.py")
|
|
126
|
+
// Try resolving dotted module path from the project root
|
|
127
|
+
if (config.language === 'python' && config.root) {
|
|
128
|
+
const modulePath = importPath.replace(/\./g, '/');
|
|
129
|
+
const fullPath = path.join(config.root, modulePath);
|
|
130
|
+
const resolved = resolveFilePath(fullPath, getExtensions('python'));
|
|
131
|
+
if (resolved) return resolved;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
return null; // External package
|
|
126
135
|
}
|
|
127
136
|
|
package/core/project.js
CHANGED
|
@@ -249,8 +249,14 @@ class ProjectIndex {
|
|
|
249
249
|
|
|
250
250
|
for (const [filePath, fileEntry] of this.files) {
|
|
251
251
|
const importedFiles = [];
|
|
252
|
+
const seenModules = new Set();
|
|
252
253
|
|
|
253
254
|
for (const importModule of fileEntry.imports) {
|
|
255
|
+
// Deduplicate: same module imported multiple times in one file
|
|
256
|
+
// (e.g., lazy imports inside different functions)
|
|
257
|
+
if (seenModules.has(importModule)) continue;
|
|
258
|
+
seenModules.add(importModule);
|
|
259
|
+
|
|
254
260
|
let resolved = resolveImport(importModule, filePath, {
|
|
255
261
|
aliases: this.config.aliases,
|
|
256
262
|
language: fileEntry.language,
|
|
@@ -344,10 +350,14 @@ class ProjectIndex {
|
|
|
344
350
|
*/
|
|
345
351
|
matchesFilters(filePath, filters = {}) {
|
|
346
352
|
// Check exclusions (patterns like 'test', 'mock', 'spec')
|
|
353
|
+
// Uses path-segment boundary matching to avoid false positives
|
|
354
|
+
// (e.g. 'test' should NOT match 'backtester', but should match 'tests/', 'test_foo', '_test.')
|
|
347
355
|
if (filters.exclude && filters.exclude.length > 0) {
|
|
348
356
|
const lowerPath = filePath.toLowerCase();
|
|
349
357
|
for (const pattern of filters.exclude) {
|
|
350
|
-
|
|
358
|
+
const escaped = pattern.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
359
|
+
const regex = new RegExp(`(^|/)${escaped}|[_.\\-]${escaped}([_.\\-/]|$)`);
|
|
360
|
+
if (regex.test(lowerPath)) {
|
|
351
361
|
return false;
|
|
352
362
|
}
|
|
353
363
|
}
|
|
@@ -1610,8 +1620,13 @@ class ProjectIndex {
|
|
|
1610
1620
|
try {
|
|
1611
1621
|
const content = fs.readFileSync(importerPath, 'utf-8');
|
|
1612
1622
|
const lines = content.split('\n');
|
|
1613
|
-
|
|
1614
|
-
|
|
1623
|
+
let targetBasename = path.basename(targetPath, path.extname(targetPath));
|
|
1624
|
+
|
|
1625
|
+
// For __init__.py, search for the package name (parent dir)
|
|
1626
|
+
// e.g., "from tools import X" → search for "tools" not "__init__"
|
|
1627
|
+
if (targetBasename === '__init__') {
|
|
1628
|
+
targetBasename = path.basename(path.dirname(targetPath));
|
|
1629
|
+
}
|
|
1615
1630
|
|
|
1616
1631
|
for (let i = 0; i < lines.length; i++) {
|
|
1617
1632
|
if (lines[i].includes(targetBasename) &&
|
|
@@ -1984,14 +1999,24 @@ class ProjectIndex {
|
|
|
1984
1999
|
mods.includes('public') && mods.includes('static');
|
|
1985
2000
|
|
|
1986
2001
|
// Python: Magic/dunder methods are called by the interpreter, not user code
|
|
1987
|
-
|
|
2002
|
+
// test_* functions/methods are called by pytest/unittest via reflection
|
|
2003
|
+
const isPythonEntryPoint = lang === 'python' &&
|
|
2004
|
+
(/^__\w+__$/.test(name) || /^test_/.test(name));
|
|
1988
2005
|
|
|
1989
2006
|
// Rust: main() is entry point, #[test] functions are called by test runner
|
|
1990
2007
|
const isRustEntryPoint = lang === 'rust' &&
|
|
1991
2008
|
(name === 'main' || mods.includes('test'));
|
|
1992
2009
|
|
|
1993
|
-
|
|
1994
|
-
|
|
2010
|
+
// Go: Test*, Benchmark*, Example* functions are called by go test
|
|
2011
|
+
const isGoTestFunc = lang === 'go' &&
|
|
2012
|
+
/^(Test|Benchmark|Example)[A-Z]/.test(name);
|
|
2013
|
+
|
|
2014
|
+
// Java: @Test annotated methods are called by JUnit
|
|
2015
|
+
const isJavaTestMethod = lang === 'java' && mods.includes('test');
|
|
2016
|
+
|
|
2017
|
+
const isEntryPoint = isGoEntryPoint || isGoTestFunc ||
|
|
2018
|
+
isJavaEntryPoint || isJavaTestMethod ||
|
|
2019
|
+
isPythonEntryPoint || isRustEntryPoint;
|
|
1995
2020
|
|
|
1996
2021
|
const isExported = fileEntry && (
|
|
1997
2022
|
fileEntry.exports.includes(name) ||
|
|
@@ -2922,10 +2947,21 @@ class ProjectIndex {
|
|
|
2922
2947
|
if (!def) {
|
|
2923
2948
|
return { found: false, function: name };
|
|
2924
2949
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2950
|
+
// For Python/Rust methods, exclude self/cls from parameter count
|
|
2951
|
+
// (callers don't pass self/cls explicitly: obj.method(a, b) not obj.method(obj, a, b))
|
|
2952
|
+
const fileEntry = this.files.get(def.file);
|
|
2953
|
+
const lang = fileEntry?.language;
|
|
2954
|
+
let params = def.paramsStructured || [];
|
|
2955
|
+
if ((lang === 'python' || lang === 'rust') && params.length > 0) {
|
|
2956
|
+
const firstName = params[0].name;
|
|
2957
|
+
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
2958
|
+
params = params.slice(1);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
const expectedParamCount = params.length;
|
|
2962
|
+
const optionalCount = params.filter(p => p.optional || p.default !== undefined).length;
|
|
2927
2963
|
const minArgs = expectedParamCount - optionalCount;
|
|
2928
|
-
const hasRest =
|
|
2964
|
+
const hasRest = params.some(p => p.rest);
|
|
2929
2965
|
|
|
2930
2966
|
// Get all call sites
|
|
2931
2967
|
const usages = this.usages(name, { codeOnly: true });
|
|
@@ -3012,11 +3048,11 @@ class ProjectIndex {
|
|
|
3012
3048
|
file: def.relativePath,
|
|
3013
3049
|
startLine: def.startLine,
|
|
3014
3050
|
signature: this.formatSignature(def),
|
|
3015
|
-
params:
|
|
3051
|
+
params: params.map(p => ({
|
|
3016
3052
|
name: p.name,
|
|
3017
3053
|
optional: p.optional || p.default !== undefined,
|
|
3018
3054
|
hasDefault: p.default !== undefined
|
|
3019
|
-
}))
|
|
3055
|
+
})),
|
|
3020
3056
|
expectedArgs: { min: minArgs, max: hasRest ? '∞' : expectedParamCount },
|
|
3021
3057
|
totalCalls: valid.length + mismatches.length + uncertain.length,
|
|
3022
3058
|
valid: valid.length,
|
package/languages/java.js
CHANGED
|
@@ -39,7 +39,10 @@ function extractModifiers(node) {
|
|
|
39
39
|
for (let i = 0; i < modifiersNode.namedChildCount; i++) {
|
|
40
40
|
const mod = modifiersNode.namedChild(i);
|
|
41
41
|
if (mod.type === 'marker_annotation' || mod.type === 'annotation') {
|
|
42
|
-
|
|
42
|
+
// Store annotation name (without @) as modifier (e.g., @Test -> 'test', @Override -> 'override')
|
|
43
|
+
const annoText = mod.text.replace(/^@/, '').split('(')[0].toLowerCase();
|
|
44
|
+
modifiers.push(annoText);
|
|
45
|
+
continue;
|
|
43
46
|
}
|
|
44
47
|
modifiers.push(mod.text);
|
|
45
48
|
}
|
package/package.json
CHANGED
package/test/parser.test.js
CHANGED
|
@@ -5603,5 +5603,255 @@ impl Rgb {
|
|
|
5603
5603
|
});
|
|
5604
5604
|
});
|
|
5605
5605
|
|
|
5606
|
+
// Regression: verify should exclude self/cls from Python method parameter count
|
|
5607
|
+
describe('Regression: verify excludes Python self/cls from param count', () => {
|
|
5608
|
+
it('should not count self as a required argument for Python methods', () => {
|
|
5609
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-verify-self-${Date.now()}`);
|
|
5610
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
5611
|
+
|
|
5612
|
+
try {
|
|
5613
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
5614
|
+
fs.writeFileSync(path.join(tmpDir, 'calculator.py'), `
|
|
5615
|
+
class Calculator:
|
|
5616
|
+
def add(self, a, b):
|
|
5617
|
+
return a + b
|
|
5618
|
+
|
|
5619
|
+
def multiply(self, x, y, z=1):
|
|
5620
|
+
return x * y * z
|
|
5621
|
+
|
|
5622
|
+
@classmethod
|
|
5623
|
+
def from_string(cls, s):
|
|
5624
|
+
return cls()
|
|
5625
|
+
`);
|
|
5626
|
+
fs.writeFileSync(path.join(tmpDir, 'main.py'), `
|
|
5627
|
+
from calculator import Calculator
|
|
5628
|
+
|
|
5629
|
+
c = Calculator()
|
|
5630
|
+
c.add(1, 2)
|
|
5631
|
+
c.add(3, 4)
|
|
5632
|
+
c.multiply(2, 3)
|
|
5633
|
+
c.multiply(2, 3, 4)
|
|
5634
|
+
Calculator.from_string("test")
|
|
5635
|
+
`);
|
|
5636
|
+
|
|
5637
|
+
const index = new ProjectIndex(tmpDir);
|
|
5638
|
+
index.build(null, { quiet: true });
|
|
5639
|
+
|
|
5640
|
+
// verify add: 2 params (not 3 counting self)
|
|
5641
|
+
const addResult = index.verify('add');
|
|
5642
|
+
assert.ok(addResult.found, 'add should be found');
|
|
5643
|
+
assert.strictEqual(addResult.expectedArgs.min, 2, 'add should expect min 2 args (not 3)');
|
|
5644
|
+
assert.strictEqual(addResult.expectedArgs.max, 2, 'add should expect max 2 args (not 3)');
|
|
5645
|
+
assert.strictEqual(addResult.mismatches, 0, `add should have 0 mismatches, got ${addResult.mismatches}`);
|
|
5646
|
+
|
|
5647
|
+
// verify multiply: 2-3 params (not 3-4 counting self)
|
|
5648
|
+
const mulResult = index.verify('multiply');
|
|
5649
|
+
assert.ok(mulResult.found, 'multiply should be found');
|
|
5650
|
+
assert.strictEqual(mulResult.expectedArgs.min, 2, 'multiply should expect min 2 args');
|
|
5651
|
+
assert.strictEqual(mulResult.expectedArgs.max, 3, 'multiply should expect max 3 args');
|
|
5652
|
+
assert.strictEqual(mulResult.mismatches, 0, `multiply should have 0 mismatches, got ${mulResult.mismatches}`);
|
|
5653
|
+
|
|
5654
|
+
// verify from_string: cls should also be excluded
|
|
5655
|
+
const clsResult = index.verify('from_string');
|
|
5656
|
+
assert.ok(clsResult.found, 'from_string should be found');
|
|
5657
|
+
assert.strictEqual(clsResult.expectedArgs.min, 1, 'from_string should expect 1 arg (not 2)');
|
|
5658
|
+
assert.strictEqual(clsResult.mismatches, 0, `from_string should have 0 mismatches, got ${clsResult.mismatches}`);
|
|
5659
|
+
|
|
5660
|
+
// params list should not include self/cls
|
|
5661
|
+
assert.ok(!addResult.params.some(p => p.name === 'self'), 'params should not include self');
|
|
5662
|
+
assert.ok(!clsResult.params.some(p => p.name === 'cls'), 'params should not include cls');
|
|
5663
|
+
} finally {
|
|
5664
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
5665
|
+
}
|
|
5666
|
+
});
|
|
5667
|
+
});
|
|
5668
|
+
|
|
5669
|
+
// Regression: deadcode should treat test_* as entry points in Python
|
|
5670
|
+
describe('Regression: deadcode treats Python test_* as entry points', () => {
|
|
5671
|
+
it('should not flag test_* functions as dead code', () => {
|
|
5672
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-deadcode-tests-${Date.now()}`);
|
|
5673
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
5674
|
+
|
|
5675
|
+
try {
|
|
5676
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
5677
|
+
fs.writeFileSync(path.join(tmpDir, 'app.py'), `
|
|
5678
|
+
def helper():
|
|
5679
|
+
return 42
|
|
5680
|
+
|
|
5681
|
+
def unused_func():
|
|
5682
|
+
return 0
|
|
5683
|
+
`);
|
|
5684
|
+
fs.writeFileSync(path.join(tmpDir, 'test_app.py'), `
|
|
5685
|
+
from app import helper
|
|
5686
|
+
|
|
5687
|
+
def test_helper_returns_42():
|
|
5688
|
+
assert helper() == 42
|
|
5689
|
+
|
|
5690
|
+
def test_helper_type():
|
|
5691
|
+
assert isinstance(helper(), int)
|
|
5692
|
+
`);
|
|
5693
|
+
|
|
5694
|
+
const index = new ProjectIndex(tmpDir);
|
|
5695
|
+
index.build(null, { quiet: true });
|
|
5696
|
+
|
|
5697
|
+
const dead = index.deadcode({ includeTests: true });
|
|
5698
|
+
const deadNames = dead.map(d => d.name);
|
|
5699
|
+
|
|
5700
|
+
// test_* functions should NOT be in dead code
|
|
5701
|
+
assert.ok(!deadNames.includes('test_helper_returns_42'),
|
|
5702
|
+
'test_helper_returns_42 should not be flagged as dead code');
|
|
5703
|
+
assert.ok(!deadNames.includes('test_helper_type'),
|
|
5704
|
+
'test_helper_type should not be flagged as dead code');
|
|
5705
|
+
|
|
5706
|
+
// unused_func should still be flagged
|
|
5707
|
+
assert.ok(deadNames.includes('unused_func'),
|
|
5708
|
+
'unused_func should be flagged as dead code');
|
|
5709
|
+
} finally {
|
|
5710
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
5711
|
+
}
|
|
5712
|
+
});
|
|
5713
|
+
});
|
|
5714
|
+
|
|
5715
|
+
// Regression: Python non-relative package imports should resolve to local files
|
|
5716
|
+
describe('Regression: Python package imports resolve to local files', () => {
|
|
5717
|
+
it('should resolve "tools.analyzer" to tools/analyzer.py', () => {
|
|
5718
|
+
const tmpDir = path.join(os.tmpdir(), `ucn-test-py-pkg-imports-${Date.now()}`);
|
|
5719
|
+
const toolsDir = path.join(tmpDir, 'tools');
|
|
5720
|
+
fs.mkdirSync(toolsDir, { recursive: true });
|
|
5721
|
+
|
|
5722
|
+
try {
|
|
5723
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
5724
|
+
fs.writeFileSync(path.join(toolsDir, '__init__.py'), '');
|
|
5725
|
+
fs.writeFileSync(path.join(toolsDir, 'analyzer.py'), `
|
|
5726
|
+
class Analyzer:
|
|
5727
|
+
def analyze(self, data):
|
|
5728
|
+
return len(data)
|
|
5729
|
+
`);
|
|
5730
|
+
fs.writeFileSync(path.join(toolsDir, 'helper.py'), `
|
|
5731
|
+
def compute():
|
|
5732
|
+
return 42
|
|
5733
|
+
`);
|
|
5734
|
+
fs.writeFileSync(path.join(tmpDir, 'main.py'), `
|
|
5735
|
+
from tools.analyzer import Analyzer
|
|
5736
|
+
from tools.helper import compute
|
|
5737
|
+
|
|
5738
|
+
a = Analyzer()
|
|
5739
|
+
a.analyze([1, 2, 3])
|
|
5740
|
+
compute()
|
|
5741
|
+
`);
|
|
5742
|
+
|
|
5743
|
+
const index = new ProjectIndex(tmpDir);
|
|
5744
|
+
index.build(null, { quiet: true });
|
|
5745
|
+
|
|
5746
|
+
// imports for main.py should resolve tools.analyzer
|
|
5747
|
+
const mainImports = index.importGraph.get(path.join(tmpDir, 'main.py')) || [];
|
|
5748
|
+
assert.ok(mainImports.some(i => i.includes('analyzer.py')),
|
|
5749
|
+
`main.py should import tools/analyzer.py, got ${mainImports.map(i => path.relative(tmpDir, i))}`);
|
|
5750
|
+
assert.ok(mainImports.some(i => i.includes('helper.py')),
|
|
5751
|
+
`main.py should import tools/helper.py, got ${mainImports.map(i => path.relative(tmpDir, i))}`);
|
|
5752
|
+
|
|
5753
|
+
// exporters for analyzer.py should include main.py
|
|
5754
|
+
const exporters = index.exporters('tools/analyzer.py');
|
|
5755
|
+
assert.ok(exporters.some(e => e.file.includes('main.py')),
|
|
5756
|
+
`tools/analyzer.py should be exported to main.py, got ${JSON.stringify(exporters)}`);
|
|
5757
|
+
} finally {
|
|
5758
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
5759
|
+
}
|
|
5760
|
+
});
|
|
5761
|
+
});
|
|
5762
|
+
|
|
5763
|
+
describe('Regression: exporters deduplicates repeated imports of same module', () => {
|
|
5764
|
+
it('should not duplicate exporters when a file imports same module multiple times', () => {
|
|
5765
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-dedup-'));
|
|
5766
|
+
const pkgDir = path.join(tmpDir, 'pkg');
|
|
5767
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
5768
|
+
|
|
5769
|
+
try {
|
|
5770
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
5771
|
+
fs.writeFileSync(path.join(pkgDir, '__init__.py'), '');
|
|
5772
|
+
fs.writeFileSync(path.join(pkgDir, 'db.py'), `
|
|
5773
|
+
def get_connection():
|
|
5774
|
+
pass
|
|
5775
|
+
|
|
5776
|
+
def insert_record():
|
|
5777
|
+
pass
|
|
5778
|
+
|
|
5779
|
+
def delete_record():
|
|
5780
|
+
pass
|
|
5781
|
+
`);
|
|
5782
|
+
// File with multiple function-body imports of same module
|
|
5783
|
+
fs.writeFileSync(path.join(tmpDir, 'app.py'), `
|
|
5784
|
+
def cmd_add():
|
|
5785
|
+
from pkg.db import get_connection, insert_record
|
|
5786
|
+
conn = get_connection()
|
|
5787
|
+
insert_record()
|
|
5788
|
+
|
|
5789
|
+
def cmd_remove():
|
|
5790
|
+
from pkg.db import get_connection, delete_record
|
|
5791
|
+
conn = get_connection()
|
|
5792
|
+
delete_record()
|
|
5793
|
+
|
|
5794
|
+
def cmd_list():
|
|
5795
|
+
from pkg.db import get_connection
|
|
5796
|
+
conn = get_connection()
|
|
5797
|
+
`);
|
|
5798
|
+
|
|
5799
|
+
const index = new ProjectIndex(tmpDir);
|
|
5800
|
+
index.build(null, { quiet: true });
|
|
5801
|
+
|
|
5802
|
+
// exporters for db.py should list app.py exactly once
|
|
5803
|
+
const exporters = index.exporters('pkg/db.py');
|
|
5804
|
+
const appEntries = exporters.filter(e => e.file.includes('app.py'));
|
|
5805
|
+
assert.strictEqual(appEntries.length, 1,
|
|
5806
|
+
`pkg/db.py should have exactly 1 exporter entry for app.py, got ${appEntries.length}`);
|
|
5807
|
+
|
|
5808
|
+
// importGraph should also be deduplicated
|
|
5809
|
+
const appImports = index.importGraph.get(path.join(tmpDir, 'app.py')) || [];
|
|
5810
|
+
const dbImports = appImports.filter(i => i.includes('db.py'));
|
|
5811
|
+
assert.strictEqual(dbImports.length, 1,
|
|
5812
|
+
`app.py importGraph should have db.py once, got ${dbImports.length}`);
|
|
5813
|
+
} finally {
|
|
5814
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
5815
|
+
}
|
|
5816
|
+
});
|
|
5817
|
+
});
|
|
5818
|
+
|
|
5819
|
+
describe('Regression: exporters shows line numbers for __init__.py', () => {
|
|
5820
|
+
it('should find import line for package __init__.py using parent dir name', () => {
|
|
5821
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-init-'));
|
|
5822
|
+
const pkgDir = path.join(tmpDir, 'mypackage');
|
|
5823
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
5824
|
+
|
|
5825
|
+
try {
|
|
5826
|
+
fs.writeFileSync(path.join(tmpDir, 'setup.py'), '');
|
|
5827
|
+
fs.writeFileSync(path.join(pkgDir, '__init__.py'), `
|
|
5828
|
+
CONFIG = {'debug': False}
|
|
5829
|
+
|
|
5830
|
+
def load_config():
|
|
5831
|
+
return CONFIG
|
|
5832
|
+
`);
|
|
5833
|
+
fs.writeFileSync(path.join(tmpDir, 'main.py'), `
|
|
5834
|
+
import os
|
|
5835
|
+
from mypackage import load_config
|
|
5836
|
+
|
|
5837
|
+
config = load_config()
|
|
5838
|
+
`);
|
|
5839
|
+
|
|
5840
|
+
const index = new ProjectIndex(tmpDir);
|
|
5841
|
+
index.build(null, { quiet: true });
|
|
5842
|
+
|
|
5843
|
+
const exporters = index.exporters('mypackage/__init__.py');
|
|
5844
|
+
const mainEntry = exporters.find(e => e.file.includes('main.py'));
|
|
5845
|
+
assert.ok(mainEntry, 'main.py should be an exporter of mypackage/__init__.py');
|
|
5846
|
+
assert.ok(mainEntry.importLine !== null,
|
|
5847
|
+
`Should find import line for __init__.py, got null`);
|
|
5848
|
+
assert.strictEqual(mainEntry.importLine, 3,
|
|
5849
|
+
`Import line should be 3 (from mypackage import ...), got ${mainEntry.importLine}`);
|
|
5850
|
+
} finally {
|
|
5851
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
5852
|
+
}
|
|
5853
|
+
});
|
|
5854
|
+
});
|
|
5855
|
+
|
|
5606
5856
|
console.log('UCN v3 Test Suite');
|
|
5607
5857
|
console.log('Run with: node --test test/parser.test.js');
|