@suss/adapter-python 0.4.0 → 0.5.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/README.md +12 -2
- package/dist/index.cjs +242 -38
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +242 -38
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -186,8 +186,18 @@ call that starts the chain is the one resolved, and the method the chain ends
|
|
|
186
186
|
with tells a read from a write. A method a base and a subclass both declare
|
|
187
187
|
gives two, and nothing is claimed.
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
A query built from a function the library exports has no project method in
|
|
190
|
+
between for its return to be read off, so a pack lists those by name and a call
|
|
191
|
+
site importing one matches on that:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
queryFunctions: ["select", "insert", "update", "delete"]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`fields` comes from the columns a query names, `User.id` in
|
|
198
|
+
`select(User.id, User.email)`. `selector` comes from the keywords the chain
|
|
199
|
+
picks rows by, `id` in `filter_by(id=user_id)`, gathered from every call in
|
|
200
|
+
the chain rather than only the last. Raw SQL handed to `execute` is not read
|
|
191
201
|
at all.
|
|
192
202
|
|
|
193
203
|
## Where a mount is written
|
package/dist/index.cjs
CHANGED
|
@@ -16233,21 +16233,22 @@ var RESOLUTION_RULES = [
|
|
|
16233
16233
|
]
|
|
16234
16234
|
),
|
|
16235
16235
|
// Which calls a function, found by the name the call is written as
|
|
16236
|
-
// rather than by resolving every callee in the project.
|
|
16237
|
-
//
|
|
16238
|
-
//
|
|
16236
|
+
// rather than by resolving every callee in the project. A caller knows
|
|
16237
|
+
// the function and wants its call sites, so both of these start from
|
|
16238
|
+
// the function. Starting from `call` instead asks what every call in
|
|
16239
|
+
// the project imports, which was 72% of everything derived.
|
|
16239
16240
|
(0, import_datalog.rule)(
|
|
16240
16241
|
"callsFunction",
|
|
16241
16242
|
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
|
|
16242
|
-
[(0, import_datalog.lit)("
|
|
16243
|
+
[(0, import_datalog.lit)("binds", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("f")), (0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c"))]
|
|
16243
16244
|
),
|
|
16244
16245
|
(0, import_datalog.rule)(
|
|
16245
16246
|
"callsFunction",
|
|
16246
16247
|
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
|
|
16247
16248
|
[
|
|
16248
|
-
(0, import_datalog.lit)("
|
|
16249
|
+
(0, import_datalog.lit)("moduleExport", (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("f")),
|
|
16249
16250
|
(0, import_datalog.lit)("imports", (0, import_datalog.variable)("c"), (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n")),
|
|
16250
|
-
(0, import_datalog.lit)("
|
|
16251
|
+
(0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c"))
|
|
16251
16252
|
]
|
|
16252
16253
|
),
|
|
16253
16254
|
// A function whose return is itself a call arrives at the same object
|
|
@@ -16941,11 +16942,85 @@ function chainsIn(calls) {
|
|
|
16941
16942
|
root: call,
|
|
16942
16943
|
last,
|
|
16943
16944
|
subject: subjectOf(call),
|
|
16944
|
-
operation: methodName(last)
|
|
16945
|
+
operation: methodName(last),
|
|
16946
|
+
between: calls.filter(
|
|
16947
|
+
(other) => other !== call && other !== last && other.startIndex >= call.startIndex && other.endIndex <= last.endIndex
|
|
16948
|
+
)
|
|
16945
16949
|
});
|
|
16946
16950
|
}
|
|
16947
16951
|
return chains;
|
|
16948
16952
|
}
|
|
16953
|
+
function importedQueryFunction(options, chain) {
|
|
16954
|
+
const callee = field(chain.root, "function");
|
|
16955
|
+
if (callee === null || callee.type !== "identifier") {
|
|
16956
|
+
return void 0;
|
|
16957
|
+
}
|
|
16958
|
+
const from = options.facts.facts("pyImportedName").find((row) => String(row[0]) === `${options.filePath}#${callee.text}`);
|
|
16959
|
+
if (from === void 0) {
|
|
16960
|
+
return void 0;
|
|
16961
|
+
}
|
|
16962
|
+
return options.patterns.find(
|
|
16963
|
+
(pattern) => String(from[1]) === pattern.module && (pattern.queryFunctions ?? []).includes(String(from[2]))
|
|
16964
|
+
);
|
|
16965
|
+
}
|
|
16966
|
+
function fieldsOf(chain) {
|
|
16967
|
+
const args = field(chain.root, "arguments");
|
|
16968
|
+
const named = [];
|
|
16969
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
16970
|
+
if (argument.type === "keyword_argument") {
|
|
16971
|
+
const name = field(argument, "name");
|
|
16972
|
+
if (name !== null) {
|
|
16973
|
+
named.push(name.text);
|
|
16974
|
+
}
|
|
16975
|
+
continue;
|
|
16976
|
+
}
|
|
16977
|
+
if (argument.type === "attribute") {
|
|
16978
|
+
const property = field(argument, "attribute");
|
|
16979
|
+
if (property !== null) {
|
|
16980
|
+
named.push(property.text);
|
|
16981
|
+
}
|
|
16982
|
+
}
|
|
16983
|
+
}
|
|
16984
|
+
return named;
|
|
16985
|
+
}
|
|
16986
|
+
function selectorOf(call) {
|
|
16987
|
+
const args = field(call, "arguments");
|
|
16988
|
+
const picked = [];
|
|
16989
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
16990
|
+
if (argument.type === "keyword_argument") {
|
|
16991
|
+
const name = field(argument, "name");
|
|
16992
|
+
if (name !== null) {
|
|
16993
|
+
picked.push(name.text);
|
|
16994
|
+
}
|
|
16995
|
+
}
|
|
16996
|
+
}
|
|
16997
|
+
return picked;
|
|
16998
|
+
}
|
|
16999
|
+
function effectFor(pattern, chain, options) {
|
|
17000
|
+
const picked = [
|
|
17001
|
+
...new Set([chain.last, ...chain.between].flatMap(selectorOf))
|
|
17002
|
+
];
|
|
17003
|
+
return {
|
|
17004
|
+
type: "interaction",
|
|
17005
|
+
binding: storageRelationalBinding({
|
|
17006
|
+
recognition: "python-storage",
|
|
17007
|
+
storageSystem: pattern.storageSystem,
|
|
17008
|
+
scope: pattern.module,
|
|
17009
|
+
table: chain.subject
|
|
17010
|
+
}),
|
|
17011
|
+
callee: chain.last.text,
|
|
17012
|
+
interaction: {
|
|
17013
|
+
class: "storage-access",
|
|
17014
|
+
kind: pattern.writes.includes(chain.operation) ? "write" : "read",
|
|
17015
|
+
fields: fieldsOf(chain),
|
|
17016
|
+
operation: chain.operation,
|
|
17017
|
+
...picked.length > 0 ? { selector: picked } : {}
|
|
17018
|
+
}
|
|
17019
|
+
};
|
|
17020
|
+
}
|
|
17021
|
+
function children3(node) {
|
|
17022
|
+
return node.namedChildren.filter((child) => child !== null);
|
|
17023
|
+
}
|
|
16949
17024
|
function fileOf(key) {
|
|
16950
17025
|
const at = key.lastIndexOf(":");
|
|
16951
17026
|
return at === -1 ? key : key.slice(0, at);
|
|
@@ -16960,26 +17035,90 @@ function returnTypeName(node) {
|
|
|
16960
17035
|
}
|
|
16961
17036
|
return annotation.type === "subscript" ? field(annotation, "value")?.text ?? null : annotation.text;
|
|
16962
17037
|
}
|
|
17038
|
+
function callsIn(node, found = []) {
|
|
17039
|
+
for (const child of node.namedChildren) {
|
|
17040
|
+
if (child === null) {
|
|
17041
|
+
continue;
|
|
17042
|
+
}
|
|
17043
|
+
if (child.type === "call") {
|
|
17044
|
+
found.push(child);
|
|
17045
|
+
}
|
|
17046
|
+
callsIn(child, found);
|
|
17047
|
+
}
|
|
17048
|
+
return found;
|
|
17049
|
+
}
|
|
17050
|
+
function storageEffects(calls, options) {
|
|
17051
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17052
|
+
const asked = new Set(
|
|
17053
|
+
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
17054
|
+
);
|
|
17055
|
+
const gather = (inCalls, filePath) => {
|
|
17056
|
+
const found = [...directStorageEffects(inCalls, { ...options, filePath })];
|
|
17057
|
+
resolveCalls(
|
|
17058
|
+
options.facts,
|
|
17059
|
+
inCalls.filter((call) => options.leadsToStorage.has(methodName(call))).map((call) => field(call, "function")).filter((callee) => callee !== null).map((callee) => calleeKeyOf(filePath, callee)).filter((key) => {
|
|
17060
|
+
const fresh = !asked.has(key);
|
|
17061
|
+
asked.add(key);
|
|
17062
|
+
return fresh;
|
|
17063
|
+
})
|
|
17064
|
+
);
|
|
17065
|
+
for (const call of inCalls) {
|
|
17066
|
+
const callee = field(call, "function");
|
|
17067
|
+
if (callee === null || !options.leadsToStorage.has(methodName(call))) {
|
|
17068
|
+
continue;
|
|
17069
|
+
}
|
|
17070
|
+
const key = resolvedOnce(options, calleeKeyOf(filePath, callee));
|
|
17071
|
+
const body = key === void 0 ? void 0 : options.definitionAt(key);
|
|
17072
|
+
if (key === void 0 || body === void 0 || seen.has(key)) {
|
|
17073
|
+
continue;
|
|
17074
|
+
}
|
|
17075
|
+
seen.add(key);
|
|
17076
|
+
const name = field(body, "name")?.text;
|
|
17077
|
+
const origin = {
|
|
17078
|
+
file: fileOf(key),
|
|
17079
|
+
line: body.startPosition.row + 1,
|
|
17080
|
+
...name === void 0 ? {} : { function: name }
|
|
17081
|
+
};
|
|
17082
|
+
found.push(
|
|
17083
|
+
...gather(callsIn(body), fileOf(key)).map(
|
|
17084
|
+
(effect) => effect.type === "interaction" && effect.origin === void 0 ? { ...effect, origin } : effect
|
|
17085
|
+
)
|
|
17086
|
+
);
|
|
17087
|
+
}
|
|
17088
|
+
return found;
|
|
17089
|
+
};
|
|
17090
|
+
return gather(calls, options.filePath);
|
|
17091
|
+
}
|
|
17092
|
+
function calleeKeyOf(filePath, callee) {
|
|
17093
|
+
return callee.type === "identifier" ? `${filePath}#${callee.text}` : nodeId(filePath, callee);
|
|
17094
|
+
}
|
|
17095
|
+
function resolvedOnce(options, calleeKey) {
|
|
17096
|
+
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === calleeKey).map((row) => String(row[1]));
|
|
17097
|
+
return resolved.length === 1 ? resolved[0] : void 0;
|
|
17098
|
+
}
|
|
16963
17099
|
function storageCallees(calls, filePath, couldMatch) {
|
|
16964
17100
|
return chainsIn(calls).filter((chain) => couldMatch.has(methodName(chain.root))).map((chain) => field(chain.root, "function")).filter((callee) => callee !== null).map((callee) => nodeId(filePath, callee));
|
|
16965
17101
|
}
|
|
16966
|
-
function
|
|
17102
|
+
function directStorageEffects(calls, options) {
|
|
16967
17103
|
if (options.patterns.length === 0) {
|
|
16968
17104
|
return [];
|
|
16969
17105
|
}
|
|
17106
|
+
const named = new Set(
|
|
17107
|
+
options.patterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
17108
|
+
);
|
|
16970
17109
|
const chains = chainsIn(calls).filter(
|
|
16971
|
-
(chain) => options.couldMatch.has(methodName(chain.root))
|
|
17110
|
+
(chain) => options.couldMatch.has(methodName(chain.root)) || named.has(methodName(chain.root))
|
|
16972
17111
|
);
|
|
16973
17112
|
if (chains.length === 0) {
|
|
16974
17113
|
return [];
|
|
16975
17114
|
}
|
|
16976
|
-
const
|
|
17115
|
+
const alreadyAsked = new Set(
|
|
16977
17116
|
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
16978
17117
|
);
|
|
16979
17118
|
resolveCalls(
|
|
16980
17119
|
options.facts,
|
|
16981
17120
|
storageCallees(calls, options.filePath, options.couldMatch).filter(
|
|
16982
|
-
(key) => !
|
|
17121
|
+
(key) => !alreadyAsked.has(key)
|
|
16983
17122
|
)
|
|
16984
17123
|
);
|
|
16985
17124
|
const effects = [];
|
|
@@ -16988,6 +17127,11 @@ function storageEffects(calls, options) {
|
|
|
16988
17127
|
if (callee === null) {
|
|
16989
17128
|
continue;
|
|
16990
17129
|
}
|
|
17130
|
+
const imported = importedQueryFunction(options, chain);
|
|
17131
|
+
if (imported !== void 0) {
|
|
17132
|
+
effects.push(effectFor(imported, chain, options));
|
|
17133
|
+
continue;
|
|
17134
|
+
}
|
|
16991
17135
|
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === nodeId(options.filePath, callee)).map((row) => String(row[1]));
|
|
16992
17136
|
const settled = resolved.length === 1 ? resolved[0] : void 0;
|
|
16993
17137
|
if (settled === void 0) {
|
|
@@ -17005,22 +17149,7 @@ function storageEffects(calls, options) {
|
|
|
17005
17149
|
if (pattern === void 0) {
|
|
17006
17150
|
continue;
|
|
17007
17151
|
}
|
|
17008
|
-
effects.push(
|
|
17009
|
-
type: "interaction",
|
|
17010
|
-
binding: storageRelationalBinding({
|
|
17011
|
-
recognition: "python-storage",
|
|
17012
|
-
storageSystem: pattern.storageSystem,
|
|
17013
|
-
scope: pattern.module,
|
|
17014
|
-
table: chain.subject
|
|
17015
|
-
}),
|
|
17016
|
-
callee: chain.last.text,
|
|
17017
|
-
interaction: {
|
|
17018
|
-
class: "storage-access",
|
|
17019
|
-
kind: pattern.writes.includes(chain.operation) ? "write" : "read",
|
|
17020
|
-
fields: [],
|
|
17021
|
-
operation: chain.operation
|
|
17022
|
-
}
|
|
17023
|
-
});
|
|
17152
|
+
effects.push(effectFor(pattern, chain, options));
|
|
17024
17153
|
}
|
|
17025
17154
|
return effects;
|
|
17026
17155
|
}
|
|
@@ -17597,7 +17726,7 @@ function buildRouteUnit(options) {
|
|
|
17597
17726
|
const branches = (perReturn ?? []).map(
|
|
17598
17727
|
(branch) => storage.length === 0 ? branch : { ...branch, extraEffects: storage }
|
|
17599
17728
|
);
|
|
17600
|
-
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0)) {
|
|
17729
|
+
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0 || effects.length > 0)) {
|
|
17601
17730
|
branches.push({
|
|
17602
17731
|
conditions: [],
|
|
17603
17732
|
terminal: {
|
|
@@ -17897,6 +18026,11 @@ function emitModuleImportFacts(db, filePath, module2, resolverOptions) {
|
|
|
17897
18026
|
moduleText,
|
|
17898
18027
|
resolution.status === "resolved" ? "resolved" : resolution.reason
|
|
17899
18028
|
]);
|
|
18029
|
+
db.add("pyImportedName", [
|
|
18030
|
+
`${filePath}#${localName}`,
|
|
18031
|
+
moduleText,
|
|
18032
|
+
binding.kind === "import" ? binding.localName : binding.importedName
|
|
18033
|
+
]);
|
|
17900
18034
|
if (resolution.status === "resolved") {
|
|
17901
18035
|
db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
|
|
17902
18036
|
db.add("imports", [
|
|
@@ -18047,6 +18181,11 @@ function unmountedReason(index) {
|
|
|
18047
18181
|
if (loops.length === 0) {
|
|
18048
18182
|
return "is never mounted through a single variable binding in the files read";
|
|
18049
18183
|
}
|
|
18184
|
+
const stopped = loops.find((loop) => loop.unmatched !== void 0);
|
|
18185
|
+
if (stopped?.unmatched !== void 0) {
|
|
18186
|
+
const { name, resolvedTo, matched, of } = stopped.unmatched;
|
|
18187
|
+
return `is not mounted by name in the files read, and a loop at ${stopped.location} mounts a list this reading resolved, where ${matched} of ${of} entries matched a router and ${name} did not (it resolved to ${resolvedTo}), so the whole list is declined rather than most of it mounted`;
|
|
18188
|
+
}
|
|
18050
18189
|
return `is not mounted by name in the files read, and ${loopsClause(loops, "mount")} routers read out of a call this reading does not follow`;
|
|
18051
18190
|
}
|
|
18052
18191
|
function rivalRegistration(index, site) {
|
|
@@ -18474,25 +18613,32 @@ function mountCallOf(stmt, scope, scan) {
|
|
|
18474
18613
|
function constructionsReturnedBy(target, index) {
|
|
18475
18614
|
const facts = index.facts;
|
|
18476
18615
|
if (facts === void 0) {
|
|
18477
|
-
return
|
|
18616
|
+
return { kind: "unread" };
|
|
18478
18617
|
}
|
|
18479
18618
|
const callKey = nodeId(target.file, target.call);
|
|
18480
18619
|
resolveCalls(facts, [callKey]);
|
|
18481
18620
|
const returned = objectReturnedBy(facts, callKey);
|
|
18482
18621
|
if (returned === null) {
|
|
18483
|
-
return
|
|
18622
|
+
return { kind: "unread" };
|
|
18484
18623
|
}
|
|
18485
18624
|
const contained = containedValues(facts, returned);
|
|
18486
18625
|
resolveCalls(facts, contained);
|
|
18487
18626
|
const found = [];
|
|
18488
18627
|
for (const value of contained) {
|
|
18489
|
-
const
|
|
18628
|
+
const resolved = resolvedValueKey(facts, value);
|
|
18629
|
+
const construction = index.byValueKey.get(resolved);
|
|
18490
18630
|
if (construction === void 0) {
|
|
18491
|
-
return
|
|
18631
|
+
return {
|
|
18632
|
+
kind: "unmatched",
|
|
18633
|
+
name: value,
|
|
18634
|
+
resolvedTo: resolved,
|
|
18635
|
+
matched: found.length,
|
|
18636
|
+
of: contained.length
|
|
18637
|
+
};
|
|
18492
18638
|
}
|
|
18493
18639
|
found.push(construction);
|
|
18494
18640
|
}
|
|
18495
|
-
return found;
|
|
18641
|
+
return { kind: "settled", found };
|
|
18496
18642
|
}
|
|
18497
18643
|
function resolvedValueKey(facts, value) {
|
|
18498
18644
|
const row = facts.facts("wantedIsWrittenAs").find((entry) => String(entry[0]) === value);
|
|
@@ -18514,10 +18660,16 @@ function mountedConstructions(arg, position, scan) {
|
|
|
18514
18660
|
return target.elements.flatMap(named);
|
|
18515
18661
|
}
|
|
18516
18662
|
if (target.kind === "call") {
|
|
18517
|
-
const
|
|
18518
|
-
if (settled.length > 0) {
|
|
18519
|
-
return
|
|
18520
|
-
}
|
|
18663
|
+
const returned = constructionsReturnedBy(target, scan.index);
|
|
18664
|
+
if (returned.kind === "settled" && returned.found.length > 0) {
|
|
18665
|
+
return returned.found;
|
|
18666
|
+
}
|
|
18667
|
+
scan.index.unenumerableLoops.set(target.location, {
|
|
18668
|
+
site: position.site,
|
|
18669
|
+
location: target.location,
|
|
18670
|
+
...returned.kind === "unmatched" ? { unmatched: returned } : {}
|
|
18671
|
+
});
|
|
18672
|
+
return [];
|
|
18521
18673
|
}
|
|
18522
18674
|
scan.index.unenumerableLoops.set(target.location, {
|
|
18523
18675
|
site: position.site,
|
|
@@ -18835,6 +18987,11 @@ async function extractPythonProject(options) {
|
|
|
18835
18987
|
}
|
|
18836
18988
|
}
|
|
18837
18989
|
const couldMatch = methodsDeclaredNear(db, storagePatterns, definitions);
|
|
18990
|
+
const startsAQuery = /* @__PURE__ */ new Set([
|
|
18991
|
+
...couldMatch,
|
|
18992
|
+
...storagePatterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
18993
|
+
]);
|
|
18994
|
+
const leadsToStorage = functionsReachingStorage(definitions, startsAQuery);
|
|
18838
18995
|
const routerIndex = buildRouterIndex(bound, options.packs, {
|
|
18839
18996
|
roots: options.roots,
|
|
18840
18997
|
...mountsRouters ? { facts: db } : {}
|
|
@@ -18852,7 +19009,8 @@ async function extractPythonProject(options) {
|
|
|
18852
19009
|
factsPath: file2,
|
|
18853
19010
|
patterns: storagePatterns,
|
|
18854
19011
|
definitionAt: (key) => definitions.get(key),
|
|
18855
|
-
couldMatch
|
|
19012
|
+
couldMatch,
|
|
19013
|
+
leadsToStorage
|
|
18856
19014
|
}
|
|
18857
19015
|
} : {}
|
|
18858
19016
|
});
|
|
@@ -18918,6 +19076,52 @@ function methodsDeclaredNear(db, patterns, definitions) {
|
|
|
18918
19076
|
}
|
|
18919
19077
|
return found;
|
|
18920
19078
|
}
|
|
19079
|
+
function calledName(call) {
|
|
19080
|
+
const callee = field(call, "function");
|
|
19081
|
+
if (callee === null) {
|
|
19082
|
+
return "";
|
|
19083
|
+
}
|
|
19084
|
+
return callee.type === "attribute" ? field(callee, "attribute")?.text ?? "" : callee.text;
|
|
19085
|
+
}
|
|
19086
|
+
function callsUnder(node, found = []) {
|
|
19087
|
+
for (const child of node.namedChildren) {
|
|
19088
|
+
if (child === null) {
|
|
19089
|
+
continue;
|
|
19090
|
+
}
|
|
19091
|
+
if (child.type === "call") {
|
|
19092
|
+
found.push(child);
|
|
19093
|
+
}
|
|
19094
|
+
callsUnder(child, found);
|
|
19095
|
+
}
|
|
19096
|
+
return found;
|
|
19097
|
+
}
|
|
19098
|
+
function functionsReachingStorage(definitions, couldMatch) {
|
|
19099
|
+
const bodies = [];
|
|
19100
|
+
for (const node of definitions.values()) {
|
|
19101
|
+
const name = field(node, "name");
|
|
19102
|
+
if (name === null) {
|
|
19103
|
+
continue;
|
|
19104
|
+
}
|
|
19105
|
+
bodies.push({ name: name.text, calls: callsUnder(node).map(calledName) });
|
|
19106
|
+
}
|
|
19107
|
+
const reaching = /* @__PURE__ */ new Set();
|
|
19108
|
+
for (const body of bodies) {
|
|
19109
|
+
if (body.calls.some((called) => couldMatch.has(called))) {
|
|
19110
|
+
reaching.add(body.name);
|
|
19111
|
+
}
|
|
19112
|
+
}
|
|
19113
|
+
for (let grew = true; grew; ) {
|
|
19114
|
+
grew = false;
|
|
19115
|
+
for (const body of bodies) {
|
|
19116
|
+
if (reaching.has(body.name) || !body.calls.some((c) => reaching.has(c))) {
|
|
19117
|
+
continue;
|
|
19118
|
+
}
|
|
19119
|
+
reaching.add(body.name);
|
|
19120
|
+
grew = true;
|
|
19121
|
+
}
|
|
19122
|
+
}
|
|
19123
|
+
return reaching;
|
|
19124
|
+
}
|
|
18921
19125
|
// Annotate the CommonJS export names for ESM import in node:
|
|
18922
19126
|
0 && (module.exports = {
|
|
18923
19127
|
annotationToShape,
|
package/dist/index.d.cts
CHANGED
|
@@ -182,6 +182,12 @@ interface StoragePattern {
|
|
|
182
182
|
queryTypes: string[];
|
|
183
183
|
/** Chain-ending methods that change what is stored. Anything else reads. */
|
|
184
184
|
writes: string[];
|
|
185
|
+
/**
|
|
186
|
+
* Functions the library exports that start a query on their own, for the
|
|
187
|
+
* case where a call site imports one rather than reaching it through a
|
|
188
|
+
* project class. `select(...)` in SQLAlchemy 2.0 is one.
|
|
189
|
+
*/
|
|
190
|
+
queryFunctions?: string[];
|
|
185
191
|
/** Which database the library is talking to, for the boundary binding. */
|
|
186
192
|
storageSystem: "postgres" | "mysql" | "sqlite";
|
|
187
193
|
}
|
|
@@ -336,16 +342,13 @@ type NoValuePrefix = "unstated" | "unreadable";
|
|
|
336
342
|
*/
|
|
337
343
|
type PathRepeatedSlashes = "kept" | "merged";
|
|
338
344
|
|
|
339
|
-
/**
|
|
340
|
-
* What discovery uses for one file. `factsPath` is the path the facts were
|
|
341
|
-
* keyed under, which is the absolute one, while a summary shows the short one.
|
|
342
|
-
*/
|
|
343
345
|
interface StorageLookup {
|
|
344
346
|
readonly facts: Database;
|
|
345
347
|
readonly factsPath: string;
|
|
346
348
|
readonly patterns: readonly StoragePattern[];
|
|
347
349
|
readonly definitionAt: (key: string) => Node | undefined;
|
|
348
350
|
readonly couldMatch: ReadonlySet<string>;
|
|
351
|
+
readonly leadsToStorage: ReadonlySet<string>;
|
|
349
352
|
}
|
|
350
353
|
|
|
351
354
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -182,6 +182,12 @@ interface StoragePattern {
|
|
|
182
182
|
queryTypes: string[];
|
|
183
183
|
/** Chain-ending methods that change what is stored. Anything else reads. */
|
|
184
184
|
writes: string[];
|
|
185
|
+
/**
|
|
186
|
+
* Functions the library exports that start a query on their own, for the
|
|
187
|
+
* case where a call site imports one rather than reaching it through a
|
|
188
|
+
* project class. `select(...)` in SQLAlchemy 2.0 is one.
|
|
189
|
+
*/
|
|
190
|
+
queryFunctions?: string[];
|
|
185
191
|
/** Which database the library is talking to, for the boundary binding. */
|
|
186
192
|
storageSystem: "postgres" | "mysql" | "sqlite";
|
|
187
193
|
}
|
|
@@ -336,16 +342,13 @@ type NoValuePrefix = "unstated" | "unreadable";
|
|
|
336
342
|
*/
|
|
337
343
|
type PathRepeatedSlashes = "kept" | "merged";
|
|
338
344
|
|
|
339
|
-
/**
|
|
340
|
-
* What discovery uses for one file. `factsPath` is the path the facts were
|
|
341
|
-
* keyed under, which is the absolute one, while a summary shows the short one.
|
|
342
|
-
*/
|
|
343
345
|
interface StorageLookup {
|
|
344
346
|
readonly facts: Database;
|
|
345
347
|
readonly factsPath: string;
|
|
346
348
|
readonly patterns: readonly StoragePattern[];
|
|
347
349
|
readonly definitionAt: (key: string) => Node | undefined;
|
|
348
350
|
readonly couldMatch: ReadonlySet<string>;
|
|
351
|
+
readonly leadsToStorage: ReadonlySet<string>;
|
|
349
352
|
}
|
|
350
353
|
|
|
351
354
|
/**
|
package/dist/index.js
CHANGED
|
@@ -16189,21 +16189,22 @@ var RESOLUTION_RULES = [
|
|
|
16189
16189
|
]
|
|
16190
16190
|
),
|
|
16191
16191
|
// Which calls a function, found by the name the call is written as
|
|
16192
|
-
// rather than by resolving every callee in the project.
|
|
16193
|
-
//
|
|
16194
|
-
//
|
|
16192
|
+
// rather than by resolving every callee in the project. A caller knows
|
|
16193
|
+
// the function and wants its call sites, so both of these start from
|
|
16194
|
+
// the function. Starting from `call` instead asks what every call in
|
|
16195
|
+
// the project imports, which was 72% of everything derived.
|
|
16195
16196
|
rule(
|
|
16196
16197
|
"callsFunction",
|
|
16197
16198
|
[v("r"), v("f")],
|
|
16198
|
-
[lit("
|
|
16199
|
+
[lit("binds", v("c"), v("f")), lit("call", v("r"), v("c"))]
|
|
16199
16200
|
),
|
|
16200
16201
|
rule(
|
|
16201
16202
|
"callsFunction",
|
|
16202
16203
|
[v("r"), v("f")],
|
|
16203
16204
|
[
|
|
16204
|
-
lit("
|
|
16205
|
+
lit("moduleExport", v("m"), v("n"), v("f")),
|
|
16205
16206
|
lit("imports", v("c"), v("m"), v("n")),
|
|
16206
|
-
lit("
|
|
16207
|
+
lit("call", v("r"), v("c"))
|
|
16207
16208
|
]
|
|
16208
16209
|
),
|
|
16209
16210
|
// A function whose return is itself a call arrives at the same object
|
|
@@ -16897,11 +16898,85 @@ function chainsIn(calls) {
|
|
|
16897
16898
|
root: call,
|
|
16898
16899
|
last,
|
|
16899
16900
|
subject: subjectOf(call),
|
|
16900
|
-
operation: methodName(last)
|
|
16901
|
+
operation: methodName(last),
|
|
16902
|
+
between: calls.filter(
|
|
16903
|
+
(other) => other !== call && other !== last && other.startIndex >= call.startIndex && other.endIndex <= last.endIndex
|
|
16904
|
+
)
|
|
16901
16905
|
});
|
|
16902
16906
|
}
|
|
16903
16907
|
return chains;
|
|
16904
16908
|
}
|
|
16909
|
+
function importedQueryFunction(options, chain) {
|
|
16910
|
+
const callee = field(chain.root, "function");
|
|
16911
|
+
if (callee === null || callee.type !== "identifier") {
|
|
16912
|
+
return void 0;
|
|
16913
|
+
}
|
|
16914
|
+
const from = options.facts.facts("pyImportedName").find((row) => String(row[0]) === `${options.filePath}#${callee.text}`);
|
|
16915
|
+
if (from === void 0) {
|
|
16916
|
+
return void 0;
|
|
16917
|
+
}
|
|
16918
|
+
return options.patterns.find(
|
|
16919
|
+
(pattern) => String(from[1]) === pattern.module && (pattern.queryFunctions ?? []).includes(String(from[2]))
|
|
16920
|
+
);
|
|
16921
|
+
}
|
|
16922
|
+
function fieldsOf(chain) {
|
|
16923
|
+
const args = field(chain.root, "arguments");
|
|
16924
|
+
const named = [];
|
|
16925
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
16926
|
+
if (argument.type === "keyword_argument") {
|
|
16927
|
+
const name = field(argument, "name");
|
|
16928
|
+
if (name !== null) {
|
|
16929
|
+
named.push(name.text);
|
|
16930
|
+
}
|
|
16931
|
+
continue;
|
|
16932
|
+
}
|
|
16933
|
+
if (argument.type === "attribute") {
|
|
16934
|
+
const property = field(argument, "attribute");
|
|
16935
|
+
if (property !== null) {
|
|
16936
|
+
named.push(property.text);
|
|
16937
|
+
}
|
|
16938
|
+
}
|
|
16939
|
+
}
|
|
16940
|
+
return named;
|
|
16941
|
+
}
|
|
16942
|
+
function selectorOf(call) {
|
|
16943
|
+
const args = field(call, "arguments");
|
|
16944
|
+
const picked = [];
|
|
16945
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
16946
|
+
if (argument.type === "keyword_argument") {
|
|
16947
|
+
const name = field(argument, "name");
|
|
16948
|
+
if (name !== null) {
|
|
16949
|
+
picked.push(name.text);
|
|
16950
|
+
}
|
|
16951
|
+
}
|
|
16952
|
+
}
|
|
16953
|
+
return picked;
|
|
16954
|
+
}
|
|
16955
|
+
function effectFor(pattern, chain, options) {
|
|
16956
|
+
const picked = [
|
|
16957
|
+
...new Set([chain.last, ...chain.between].flatMap(selectorOf))
|
|
16958
|
+
];
|
|
16959
|
+
return {
|
|
16960
|
+
type: "interaction",
|
|
16961
|
+
binding: storageRelationalBinding({
|
|
16962
|
+
recognition: "python-storage",
|
|
16963
|
+
storageSystem: pattern.storageSystem,
|
|
16964
|
+
scope: pattern.module,
|
|
16965
|
+
table: chain.subject
|
|
16966
|
+
}),
|
|
16967
|
+
callee: chain.last.text,
|
|
16968
|
+
interaction: {
|
|
16969
|
+
class: "storage-access",
|
|
16970
|
+
kind: pattern.writes.includes(chain.operation) ? "write" : "read",
|
|
16971
|
+
fields: fieldsOf(chain),
|
|
16972
|
+
operation: chain.operation,
|
|
16973
|
+
...picked.length > 0 ? { selector: picked } : {}
|
|
16974
|
+
}
|
|
16975
|
+
};
|
|
16976
|
+
}
|
|
16977
|
+
function children3(node) {
|
|
16978
|
+
return node.namedChildren.filter((child) => child !== null);
|
|
16979
|
+
}
|
|
16905
16980
|
function fileOf(key) {
|
|
16906
16981
|
const at = key.lastIndexOf(":");
|
|
16907
16982
|
return at === -1 ? key : key.slice(0, at);
|
|
@@ -16916,26 +16991,90 @@ function returnTypeName(node) {
|
|
|
16916
16991
|
}
|
|
16917
16992
|
return annotation.type === "subscript" ? field(annotation, "value")?.text ?? null : annotation.text;
|
|
16918
16993
|
}
|
|
16994
|
+
function callsIn(node, found = []) {
|
|
16995
|
+
for (const child of node.namedChildren) {
|
|
16996
|
+
if (child === null) {
|
|
16997
|
+
continue;
|
|
16998
|
+
}
|
|
16999
|
+
if (child.type === "call") {
|
|
17000
|
+
found.push(child);
|
|
17001
|
+
}
|
|
17002
|
+
callsIn(child, found);
|
|
17003
|
+
}
|
|
17004
|
+
return found;
|
|
17005
|
+
}
|
|
17006
|
+
function storageEffects(calls, options) {
|
|
17007
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17008
|
+
const asked = new Set(
|
|
17009
|
+
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
17010
|
+
);
|
|
17011
|
+
const gather = (inCalls, filePath) => {
|
|
17012
|
+
const found = [...directStorageEffects(inCalls, { ...options, filePath })];
|
|
17013
|
+
resolveCalls(
|
|
17014
|
+
options.facts,
|
|
17015
|
+
inCalls.filter((call) => options.leadsToStorage.has(methodName(call))).map((call) => field(call, "function")).filter((callee) => callee !== null).map((callee) => calleeKeyOf(filePath, callee)).filter((key) => {
|
|
17016
|
+
const fresh = !asked.has(key);
|
|
17017
|
+
asked.add(key);
|
|
17018
|
+
return fresh;
|
|
17019
|
+
})
|
|
17020
|
+
);
|
|
17021
|
+
for (const call of inCalls) {
|
|
17022
|
+
const callee = field(call, "function");
|
|
17023
|
+
if (callee === null || !options.leadsToStorage.has(methodName(call))) {
|
|
17024
|
+
continue;
|
|
17025
|
+
}
|
|
17026
|
+
const key = resolvedOnce(options, calleeKeyOf(filePath, callee));
|
|
17027
|
+
const body = key === void 0 ? void 0 : options.definitionAt(key);
|
|
17028
|
+
if (key === void 0 || body === void 0 || seen.has(key)) {
|
|
17029
|
+
continue;
|
|
17030
|
+
}
|
|
17031
|
+
seen.add(key);
|
|
17032
|
+
const name = field(body, "name")?.text;
|
|
17033
|
+
const origin = {
|
|
17034
|
+
file: fileOf(key),
|
|
17035
|
+
line: body.startPosition.row + 1,
|
|
17036
|
+
...name === void 0 ? {} : { function: name }
|
|
17037
|
+
};
|
|
17038
|
+
found.push(
|
|
17039
|
+
...gather(callsIn(body), fileOf(key)).map(
|
|
17040
|
+
(effect) => effect.type === "interaction" && effect.origin === void 0 ? { ...effect, origin } : effect
|
|
17041
|
+
)
|
|
17042
|
+
);
|
|
17043
|
+
}
|
|
17044
|
+
return found;
|
|
17045
|
+
};
|
|
17046
|
+
return gather(calls, options.filePath);
|
|
17047
|
+
}
|
|
17048
|
+
function calleeKeyOf(filePath, callee) {
|
|
17049
|
+
return callee.type === "identifier" ? `${filePath}#${callee.text}` : nodeId(filePath, callee);
|
|
17050
|
+
}
|
|
17051
|
+
function resolvedOnce(options, calleeKey) {
|
|
17052
|
+
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === calleeKey).map((row) => String(row[1]));
|
|
17053
|
+
return resolved.length === 1 ? resolved[0] : void 0;
|
|
17054
|
+
}
|
|
16919
17055
|
function storageCallees(calls, filePath, couldMatch) {
|
|
16920
17056
|
return chainsIn(calls).filter((chain) => couldMatch.has(methodName(chain.root))).map((chain) => field(chain.root, "function")).filter((callee) => callee !== null).map((callee) => nodeId(filePath, callee));
|
|
16921
17057
|
}
|
|
16922
|
-
function
|
|
17058
|
+
function directStorageEffects(calls, options) {
|
|
16923
17059
|
if (options.patterns.length === 0) {
|
|
16924
17060
|
return [];
|
|
16925
17061
|
}
|
|
17062
|
+
const named = new Set(
|
|
17063
|
+
options.patterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
17064
|
+
);
|
|
16926
17065
|
const chains = chainsIn(calls).filter(
|
|
16927
|
-
(chain) => options.couldMatch.has(methodName(chain.root))
|
|
17066
|
+
(chain) => options.couldMatch.has(methodName(chain.root)) || named.has(methodName(chain.root))
|
|
16928
17067
|
);
|
|
16929
17068
|
if (chains.length === 0) {
|
|
16930
17069
|
return [];
|
|
16931
17070
|
}
|
|
16932
|
-
const
|
|
17071
|
+
const alreadyAsked = new Set(
|
|
16933
17072
|
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
16934
17073
|
);
|
|
16935
17074
|
resolveCalls(
|
|
16936
17075
|
options.facts,
|
|
16937
17076
|
storageCallees(calls, options.filePath, options.couldMatch).filter(
|
|
16938
|
-
(key) => !
|
|
17077
|
+
(key) => !alreadyAsked.has(key)
|
|
16939
17078
|
)
|
|
16940
17079
|
);
|
|
16941
17080
|
const effects = [];
|
|
@@ -16944,6 +17083,11 @@ function storageEffects(calls, options) {
|
|
|
16944
17083
|
if (callee === null) {
|
|
16945
17084
|
continue;
|
|
16946
17085
|
}
|
|
17086
|
+
const imported = importedQueryFunction(options, chain);
|
|
17087
|
+
if (imported !== void 0) {
|
|
17088
|
+
effects.push(effectFor(imported, chain, options));
|
|
17089
|
+
continue;
|
|
17090
|
+
}
|
|
16947
17091
|
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === nodeId(options.filePath, callee)).map((row) => String(row[1]));
|
|
16948
17092
|
const settled = resolved.length === 1 ? resolved[0] : void 0;
|
|
16949
17093
|
if (settled === void 0) {
|
|
@@ -16961,22 +17105,7 @@ function storageEffects(calls, options) {
|
|
|
16961
17105
|
if (pattern === void 0) {
|
|
16962
17106
|
continue;
|
|
16963
17107
|
}
|
|
16964
|
-
effects.push(
|
|
16965
|
-
type: "interaction",
|
|
16966
|
-
binding: storageRelationalBinding({
|
|
16967
|
-
recognition: "python-storage",
|
|
16968
|
-
storageSystem: pattern.storageSystem,
|
|
16969
|
-
scope: pattern.module,
|
|
16970
|
-
table: chain.subject
|
|
16971
|
-
}),
|
|
16972
|
-
callee: chain.last.text,
|
|
16973
|
-
interaction: {
|
|
16974
|
-
class: "storage-access",
|
|
16975
|
-
kind: pattern.writes.includes(chain.operation) ? "write" : "read",
|
|
16976
|
-
fields: [],
|
|
16977
|
-
operation: chain.operation
|
|
16978
|
-
}
|
|
16979
|
-
});
|
|
17108
|
+
effects.push(effectFor(pattern, chain, options));
|
|
16980
17109
|
}
|
|
16981
17110
|
return effects;
|
|
16982
17111
|
}
|
|
@@ -17553,7 +17682,7 @@ function buildRouteUnit(options) {
|
|
|
17553
17682
|
const branches = (perReturn ?? []).map(
|
|
17554
17683
|
(branch) => storage.length === 0 ? branch : { ...branch, extraEffects: storage }
|
|
17555
17684
|
);
|
|
17556
|
-
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0)) {
|
|
17685
|
+
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0 || effects.length > 0)) {
|
|
17557
17686
|
branches.push({
|
|
17558
17687
|
conditions: [],
|
|
17559
17688
|
terminal: {
|
|
@@ -17853,6 +17982,11 @@ function emitModuleImportFacts(db, filePath, module, resolverOptions) {
|
|
|
17853
17982
|
moduleText,
|
|
17854
17983
|
resolution.status === "resolved" ? "resolved" : resolution.reason
|
|
17855
17984
|
]);
|
|
17985
|
+
db.add("pyImportedName", [
|
|
17986
|
+
`${filePath}#${localName}`,
|
|
17987
|
+
moduleText,
|
|
17988
|
+
binding.kind === "import" ? binding.localName : binding.importedName
|
|
17989
|
+
]);
|
|
17856
17990
|
if (resolution.status === "resolved") {
|
|
17857
17991
|
db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
|
|
17858
17992
|
db.add("imports", [
|
|
@@ -18003,6 +18137,11 @@ function unmountedReason(index) {
|
|
|
18003
18137
|
if (loops.length === 0) {
|
|
18004
18138
|
return "is never mounted through a single variable binding in the files read";
|
|
18005
18139
|
}
|
|
18140
|
+
const stopped = loops.find((loop) => loop.unmatched !== void 0);
|
|
18141
|
+
if (stopped?.unmatched !== void 0) {
|
|
18142
|
+
const { name, resolvedTo, matched, of } = stopped.unmatched;
|
|
18143
|
+
return `is not mounted by name in the files read, and a loop at ${stopped.location} mounts a list this reading resolved, where ${matched} of ${of} entries matched a router and ${name} did not (it resolved to ${resolvedTo}), so the whole list is declined rather than most of it mounted`;
|
|
18144
|
+
}
|
|
18006
18145
|
return `is not mounted by name in the files read, and ${loopsClause(loops, "mount")} routers read out of a call this reading does not follow`;
|
|
18007
18146
|
}
|
|
18008
18147
|
function rivalRegistration(index, site) {
|
|
@@ -18430,25 +18569,32 @@ function mountCallOf(stmt, scope, scan) {
|
|
|
18430
18569
|
function constructionsReturnedBy(target, index) {
|
|
18431
18570
|
const facts = index.facts;
|
|
18432
18571
|
if (facts === void 0) {
|
|
18433
|
-
return
|
|
18572
|
+
return { kind: "unread" };
|
|
18434
18573
|
}
|
|
18435
18574
|
const callKey = nodeId(target.file, target.call);
|
|
18436
18575
|
resolveCalls(facts, [callKey]);
|
|
18437
18576
|
const returned = objectReturnedBy(facts, callKey);
|
|
18438
18577
|
if (returned === null) {
|
|
18439
|
-
return
|
|
18578
|
+
return { kind: "unread" };
|
|
18440
18579
|
}
|
|
18441
18580
|
const contained = containedValues(facts, returned);
|
|
18442
18581
|
resolveCalls(facts, contained);
|
|
18443
18582
|
const found = [];
|
|
18444
18583
|
for (const value of contained) {
|
|
18445
|
-
const
|
|
18584
|
+
const resolved = resolvedValueKey(facts, value);
|
|
18585
|
+
const construction = index.byValueKey.get(resolved);
|
|
18446
18586
|
if (construction === void 0) {
|
|
18447
|
-
return
|
|
18587
|
+
return {
|
|
18588
|
+
kind: "unmatched",
|
|
18589
|
+
name: value,
|
|
18590
|
+
resolvedTo: resolved,
|
|
18591
|
+
matched: found.length,
|
|
18592
|
+
of: contained.length
|
|
18593
|
+
};
|
|
18448
18594
|
}
|
|
18449
18595
|
found.push(construction);
|
|
18450
18596
|
}
|
|
18451
|
-
return found;
|
|
18597
|
+
return { kind: "settled", found };
|
|
18452
18598
|
}
|
|
18453
18599
|
function resolvedValueKey(facts, value) {
|
|
18454
18600
|
const row = facts.facts("wantedIsWrittenAs").find((entry) => String(entry[0]) === value);
|
|
@@ -18470,10 +18616,16 @@ function mountedConstructions(arg, position, scan) {
|
|
|
18470
18616
|
return target.elements.flatMap(named);
|
|
18471
18617
|
}
|
|
18472
18618
|
if (target.kind === "call") {
|
|
18473
|
-
const
|
|
18474
|
-
if (settled.length > 0) {
|
|
18475
|
-
return
|
|
18476
|
-
}
|
|
18619
|
+
const returned = constructionsReturnedBy(target, scan.index);
|
|
18620
|
+
if (returned.kind === "settled" && returned.found.length > 0) {
|
|
18621
|
+
return returned.found;
|
|
18622
|
+
}
|
|
18623
|
+
scan.index.unenumerableLoops.set(target.location, {
|
|
18624
|
+
site: position.site,
|
|
18625
|
+
location: target.location,
|
|
18626
|
+
...returned.kind === "unmatched" ? { unmatched: returned } : {}
|
|
18627
|
+
});
|
|
18628
|
+
return [];
|
|
18477
18629
|
}
|
|
18478
18630
|
scan.index.unenumerableLoops.set(target.location, {
|
|
18479
18631
|
site: position.site,
|
|
@@ -18791,6 +18943,11 @@ async function extractPythonProject(options) {
|
|
|
18791
18943
|
}
|
|
18792
18944
|
}
|
|
18793
18945
|
const couldMatch = methodsDeclaredNear(db, storagePatterns, definitions);
|
|
18946
|
+
const startsAQuery = /* @__PURE__ */ new Set([
|
|
18947
|
+
...couldMatch,
|
|
18948
|
+
...storagePatterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
18949
|
+
]);
|
|
18950
|
+
const leadsToStorage = functionsReachingStorage(definitions, startsAQuery);
|
|
18794
18951
|
const routerIndex = buildRouterIndex(bound, options.packs, {
|
|
18795
18952
|
roots: options.roots,
|
|
18796
18953
|
...mountsRouters ? { facts: db } : {}
|
|
@@ -18808,7 +18965,8 @@ async function extractPythonProject(options) {
|
|
|
18808
18965
|
factsPath: file2,
|
|
18809
18966
|
patterns: storagePatterns,
|
|
18810
18967
|
definitionAt: (key) => definitions.get(key),
|
|
18811
|
-
couldMatch
|
|
18968
|
+
couldMatch,
|
|
18969
|
+
leadsToStorage
|
|
18812
18970
|
}
|
|
18813
18971
|
} : {}
|
|
18814
18972
|
});
|
|
@@ -18874,6 +19032,52 @@ function methodsDeclaredNear(db, patterns, definitions) {
|
|
|
18874
19032
|
}
|
|
18875
19033
|
return found;
|
|
18876
19034
|
}
|
|
19035
|
+
function calledName(call) {
|
|
19036
|
+
const callee = field(call, "function");
|
|
19037
|
+
if (callee === null) {
|
|
19038
|
+
return "";
|
|
19039
|
+
}
|
|
19040
|
+
return callee.type === "attribute" ? field(callee, "attribute")?.text ?? "" : callee.text;
|
|
19041
|
+
}
|
|
19042
|
+
function callsUnder(node, found = []) {
|
|
19043
|
+
for (const child of node.namedChildren) {
|
|
19044
|
+
if (child === null) {
|
|
19045
|
+
continue;
|
|
19046
|
+
}
|
|
19047
|
+
if (child.type === "call") {
|
|
19048
|
+
found.push(child);
|
|
19049
|
+
}
|
|
19050
|
+
callsUnder(child, found);
|
|
19051
|
+
}
|
|
19052
|
+
return found;
|
|
19053
|
+
}
|
|
19054
|
+
function functionsReachingStorage(definitions, couldMatch) {
|
|
19055
|
+
const bodies = [];
|
|
19056
|
+
for (const node of definitions.values()) {
|
|
19057
|
+
const name = field(node, "name");
|
|
19058
|
+
if (name === null) {
|
|
19059
|
+
continue;
|
|
19060
|
+
}
|
|
19061
|
+
bodies.push({ name: name.text, calls: callsUnder(node).map(calledName) });
|
|
19062
|
+
}
|
|
19063
|
+
const reaching = /* @__PURE__ */ new Set();
|
|
19064
|
+
for (const body of bodies) {
|
|
19065
|
+
if (body.calls.some((called) => couldMatch.has(called))) {
|
|
19066
|
+
reaching.add(body.name);
|
|
19067
|
+
}
|
|
19068
|
+
}
|
|
19069
|
+
for (let grew = true; grew; ) {
|
|
19070
|
+
grew = false;
|
|
19071
|
+
for (const body of bodies) {
|
|
19072
|
+
if (reaching.has(body.name) || !body.calls.some((c) => reaching.has(c))) {
|
|
19073
|
+
continue;
|
|
19074
|
+
}
|
|
19075
|
+
reaching.add(body.name);
|
|
19076
|
+
grew = true;
|
|
19077
|
+
}
|
|
19078
|
+
}
|
|
19079
|
+
return reaching;
|
|
19080
|
+
}
|
|
18877
19081
|
export {
|
|
18878
19082
|
annotationToShape,
|
|
18879
19083
|
bindModule,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/adapter-python",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Python language adapter for suss — parses source with tree-sitter (WASM), resolves names with a lexical binder, and emits raw extractions for the suss pipeline.",
|
|
6
6
|
"keywords": [
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"typecheck": "tsc --noEmit"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@suss/behavioral-ir": "0.
|
|
51
|
-
"@suss/datalog": "0.
|
|
52
|
-
"@suss/extractor": "0.
|
|
50
|
+
"@suss/behavioral-ir": "0.5.0",
|
|
51
|
+
"@suss/datalog": "0.5.0",
|
|
52
|
+
"@suss/extractor": "0.5.0",
|
|
53
53
|
"web-tree-sitter": "^0.26.11"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|