ucn 3.4.1 → 3.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/project.js +43 -0
- package/package.json +1 -1
- package/test/parser.test.js +46 -0
package/core/project.js
CHANGED
|
@@ -1021,6 +1021,14 @@ class ProjectIndex {
|
|
|
1021
1021
|
const matchesDef = definitions.some(d => d.className === targetClass);
|
|
1022
1022
|
if (!matchesDef) continue;
|
|
1023
1023
|
// Falls through to add as caller
|
|
1024
|
+
} else if (fileEntry.language === 'python' && ['self', 'cls'].includes(call.receiver)) {
|
|
1025
|
+
// self.method() / cls.method() — resolve to same-class method
|
|
1026
|
+
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
1027
|
+
if (!callerSymbol?.className) continue;
|
|
1028
|
+
// Check if any definition of searched function belongs to caller's class
|
|
1029
|
+
const matchesDef = definitions.some(d => d.className === callerSymbol.className);
|
|
1030
|
+
if (!matchesDef) continue;
|
|
1031
|
+
// Falls through to add as caller
|
|
1024
1032
|
} else {
|
|
1025
1033
|
// Always skip this/self/cls calls (internal state access, not function calls)
|
|
1026
1034
|
if (['this', 'self', 'cls'].includes(call.receiver)) continue;
|
|
@@ -1125,6 +1133,7 @@ class ProjectIndex {
|
|
|
1125
1133
|
|
|
1126
1134
|
const callees = new Map(); // key -> { name, bindingId, count }
|
|
1127
1135
|
let selfAttrCalls = null; // collected for Python self.attr.method() resolution
|
|
1136
|
+
let selfMethodCalls = null; // collected for Python self.method() resolution
|
|
1128
1137
|
|
|
1129
1138
|
for (const call of calls) {
|
|
1130
1139
|
// Filter to calls within this function's scope using enclosingFunction
|
|
@@ -1134,11 +1143,14 @@ class ProjectIndex {
|
|
|
1134
1143
|
|
|
1135
1144
|
// Smart method call handling:
|
|
1136
1145
|
// - Go: include all method calls (Go doesn't use this/self/cls)
|
|
1146
|
+
// - Python self.method(): resolve to same-class method (handled below)
|
|
1137
1147
|
// - Python self.attr.method(): resolve via selfAttribute (handled below)
|
|
1138
1148
|
// - Other languages: skip method calls unless explicitly requested
|
|
1139
1149
|
if (call.isMethod) {
|
|
1140
1150
|
if (call.selfAttribute && language === 'python') {
|
|
1141
1151
|
// Will be resolved in second pass below
|
|
1152
|
+
} else if (language === 'python' && ['self', 'cls'].includes(call.receiver)) {
|
|
1153
|
+
// self.method() / cls.method() — resolve to same-class method below
|
|
1142
1154
|
} else if (language !== 'go' && !options.includeMethods) {
|
|
1143
1155
|
continue;
|
|
1144
1156
|
}
|
|
@@ -1154,6 +1166,13 @@ class ProjectIndex {
|
|
|
1154
1166
|
continue;
|
|
1155
1167
|
}
|
|
1156
1168
|
|
|
1169
|
+
// Collect Python self.method() calls for same-class resolution
|
|
1170
|
+
if (language === 'python' && call.isMethod && ['self', 'cls'].includes(call.receiver)) {
|
|
1171
|
+
if (!selfMethodCalls) selfMethodCalls = [];
|
|
1172
|
+
selfMethodCalls.push(call);
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1157
1176
|
// Resolve binding within this file (without mutating cached call objects)
|
|
1158
1177
|
let calleeKey = call.bindingId || call.name;
|
|
1159
1178
|
let bindingResolved = call.bindingId;
|
|
@@ -1236,6 +1255,30 @@ class ProjectIndex {
|
|
|
1236
1255
|
}
|
|
1237
1256
|
}
|
|
1238
1257
|
|
|
1258
|
+
// Third pass: resolve Python self.method() calls to same-class methods
|
|
1259
|
+
if (selfMethodCalls && def.className) {
|
|
1260
|
+
for (const call of selfMethodCalls) {
|
|
1261
|
+
const symbols = this.symbols.get(call.name);
|
|
1262
|
+
if (!symbols) continue;
|
|
1263
|
+
|
|
1264
|
+
// Find method in same class
|
|
1265
|
+
const match = symbols.find(s => s.className === def.className);
|
|
1266
|
+
if (!match) continue;
|
|
1267
|
+
|
|
1268
|
+
const key = match.bindingId || `${def.className}.${call.name}`;
|
|
1269
|
+
const existing = callees.get(key);
|
|
1270
|
+
if (existing) {
|
|
1271
|
+
existing.count += 1;
|
|
1272
|
+
} else {
|
|
1273
|
+
callees.set(key, {
|
|
1274
|
+
name: call.name,
|
|
1275
|
+
bindingId: match.bindingId,
|
|
1276
|
+
count: 1
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1239
1282
|
// Look up each callee in the symbol table
|
|
1240
1283
|
// For methods, prefer callees from: 1) same file, 2) same package, 3) same receiver type
|
|
1241
1284
|
const result = [];
|
package/package.json
CHANGED
package/test/parser.test.js
CHANGED
|
@@ -6188,5 +6188,51 @@ class Line:
|
|
|
6188
6188
|
});
|
|
6189
6189
|
});
|
|
6190
6190
|
|
|
6191
|
+
describe('Regression: Python self.method() same-class resolution', () => {
|
|
6192
|
+
it('findCallees should resolve self.method() to same-class methods', () => {
|
|
6193
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ucn-selfmethod-'));
|
|
6194
|
+
try {
|
|
6195
|
+
fs.writeFileSync(path.join(tmpDir, 'service.py'), `
|
|
6196
|
+
class DataService:
|
|
6197
|
+
def _fetch_remote(self, key, days):
|
|
6198
|
+
return self._make_request(f"/api/{key}")
|
|
6199
|
+
|
|
6200
|
+
def _make_request(self, url):
|
|
6201
|
+
return None
|
|
6202
|
+
|
|
6203
|
+
def get_records(self, key, days=365):
|
|
6204
|
+
if self._is_valid(key):
|
|
6205
|
+
return self._fetch_remote(key, days)
|
|
6206
|
+
return None
|
|
6207
|
+
|
|
6208
|
+
def _is_valid(self, key):
|
|
6209
|
+
return len(key) > 0
|
|
6210
|
+
`);
|
|
6211
|
+
const index = new ProjectIndex(tmpDir);
|
|
6212
|
+
index.build('**/*.py', { quiet: true });
|
|
6213
|
+
|
|
6214
|
+
// get_records should have _fetch_remote and _is_valid as callees
|
|
6215
|
+
const defs = index.symbols.get('get_records');
|
|
6216
|
+
assert.ok(defs && defs.length > 0, 'Should find get_records');
|
|
6217
|
+
const callees = index.findCallees(defs[0]);
|
|
6218
|
+
const calleeNames = callees.map(c => c.name);
|
|
6219
|
+
assert.ok(calleeNames.includes('_fetch_remote'),
|
|
6220
|
+
`Should resolve self._fetch_remote(), got: ${calleeNames.join(', ')}`);
|
|
6221
|
+
assert.ok(calleeNames.includes('_is_valid'),
|
|
6222
|
+
`Should resolve self._is_valid(), got: ${calleeNames.join(', ')}`);
|
|
6223
|
+
|
|
6224
|
+
// _fetch_remote should have get_records as caller
|
|
6225
|
+
const fetchDefs = index.symbols.get('_fetch_remote');
|
|
6226
|
+
assert.ok(fetchDefs && fetchDefs.length > 0, 'Should find _fetch_remote');
|
|
6227
|
+
const callers = index.findCallers('_fetch_remote');
|
|
6228
|
+
const callerNames = callers.map(c => c.callerName);
|
|
6229
|
+
assert.ok(callerNames.includes('get_records'),
|
|
6230
|
+
`Should find get_records as caller of _fetch_remote, got: ${callerNames.join(', ')}`);
|
|
6231
|
+
} finally {
|
|
6232
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
6233
|
+
}
|
|
6234
|
+
});
|
|
6235
|
+
});
|
|
6236
|
+
|
|
6191
6237
|
console.log('UCN v3 Test Suite');
|
|
6192
6238
|
console.log('Run with: node --test test/parser.test.js');
|