@suss/adapter-python 0.4.0 → 0.5.1
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 +257 -39
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +257 -39
- 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
|
@@ -994,12 +994,26 @@ function argsOf(call) {
|
|
|
994
994
|
function calleeText(call) {
|
|
995
995
|
return field(call, "function")?.text ?? call.text;
|
|
996
996
|
}
|
|
997
|
+
function withoutChainLinks(calls) {
|
|
998
|
+
const isLink = /* @__PURE__ */ new Set();
|
|
999
|
+
for (const call of calls) {
|
|
1000
|
+
const callee = field(call, "function");
|
|
1001
|
+
if (callee === null || callee.type !== "attribute") {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
const object2 = field(callee, "object");
|
|
1005
|
+
if (object2 !== null && object2.type === "call") {
|
|
1006
|
+
isLink.add(object2.id);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return calls.filter((call) => !isLink.has(call.id));
|
|
1010
|
+
}
|
|
997
1011
|
function invocationEffects(definitionNode) {
|
|
998
1012
|
const body = field(definitionNode, "body");
|
|
999
1013
|
if (body === null) {
|
|
1000
1014
|
return [];
|
|
1001
1015
|
}
|
|
1002
|
-
const calls = bodyCalls(body);
|
|
1016
|
+
const calls = withoutChainLinks(bodyCalls(body));
|
|
1003
1017
|
if (calls.length === 0) {
|
|
1004
1018
|
return [];
|
|
1005
1019
|
}
|
|
@@ -16233,21 +16247,22 @@ var RESOLUTION_RULES = [
|
|
|
16233
16247
|
]
|
|
16234
16248
|
),
|
|
16235
16249
|
// 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
|
-
//
|
|
16250
|
+
// rather than by resolving every callee in the project. A caller knows
|
|
16251
|
+
// the function and wants its call sites, so both of these start from
|
|
16252
|
+
// the function. Starting from `call` instead asks what every call in
|
|
16253
|
+
// the project imports, which was 72% of everything derived.
|
|
16239
16254
|
(0, import_datalog.rule)(
|
|
16240
16255
|
"callsFunction",
|
|
16241
16256
|
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
|
|
16242
|
-
[(0, import_datalog.lit)("
|
|
16257
|
+
[(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
16258
|
),
|
|
16244
16259
|
(0, import_datalog.rule)(
|
|
16245
16260
|
"callsFunction",
|
|
16246
16261
|
[(0, import_datalog.variable)("r"), (0, import_datalog.variable)("f")],
|
|
16247
16262
|
[
|
|
16248
|
-
(0, import_datalog.lit)("
|
|
16263
|
+
(0, import_datalog.lit)("moduleExport", (0, import_datalog.variable)("m"), (0, import_datalog.variable)("n"), (0, import_datalog.variable)("f")),
|
|
16249
16264
|
(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)("
|
|
16265
|
+
(0, import_datalog.lit)("call", (0, import_datalog.variable)("r"), (0, import_datalog.variable)("c"))
|
|
16251
16266
|
]
|
|
16252
16267
|
),
|
|
16253
16268
|
// A function whose return is itself a call arrives at the same object
|
|
@@ -16941,11 +16956,85 @@ function chainsIn(calls) {
|
|
|
16941
16956
|
root: call,
|
|
16942
16957
|
last,
|
|
16943
16958
|
subject: subjectOf(call),
|
|
16944
|
-
operation: methodName(last)
|
|
16959
|
+
operation: methodName(last),
|
|
16960
|
+
between: calls.filter(
|
|
16961
|
+
(other) => other !== call && other !== last && other.startIndex >= call.startIndex && other.endIndex <= last.endIndex
|
|
16962
|
+
)
|
|
16945
16963
|
});
|
|
16946
16964
|
}
|
|
16947
16965
|
return chains;
|
|
16948
16966
|
}
|
|
16967
|
+
function importedQueryFunction(options, chain) {
|
|
16968
|
+
const callee = field(chain.root, "function");
|
|
16969
|
+
if (callee === null || callee.type !== "identifier") {
|
|
16970
|
+
return void 0;
|
|
16971
|
+
}
|
|
16972
|
+
const from = options.facts.facts("pyImportedName").find((row) => String(row[0]) === `${options.filePath}#${callee.text}`);
|
|
16973
|
+
if (from === void 0) {
|
|
16974
|
+
return void 0;
|
|
16975
|
+
}
|
|
16976
|
+
return options.patterns.find(
|
|
16977
|
+
(pattern) => String(from[1]) === pattern.module && (pattern.queryFunctions ?? []).includes(String(from[2]))
|
|
16978
|
+
);
|
|
16979
|
+
}
|
|
16980
|
+
function fieldsOf(chain) {
|
|
16981
|
+
const args = field(chain.root, "arguments");
|
|
16982
|
+
const named = [];
|
|
16983
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
16984
|
+
if (argument.type === "keyword_argument") {
|
|
16985
|
+
const name = field(argument, "name");
|
|
16986
|
+
if (name !== null) {
|
|
16987
|
+
named.push(name.text);
|
|
16988
|
+
}
|
|
16989
|
+
continue;
|
|
16990
|
+
}
|
|
16991
|
+
if (argument.type === "attribute") {
|
|
16992
|
+
const property = field(argument, "attribute");
|
|
16993
|
+
if (property !== null) {
|
|
16994
|
+
named.push(property.text);
|
|
16995
|
+
}
|
|
16996
|
+
}
|
|
16997
|
+
}
|
|
16998
|
+
return named;
|
|
16999
|
+
}
|
|
17000
|
+
function selectorOf(call) {
|
|
17001
|
+
const args = field(call, "arguments");
|
|
17002
|
+
const picked = [];
|
|
17003
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
17004
|
+
if (argument.type === "keyword_argument") {
|
|
17005
|
+
const name = field(argument, "name");
|
|
17006
|
+
if (name !== null) {
|
|
17007
|
+
picked.push(name.text);
|
|
17008
|
+
}
|
|
17009
|
+
}
|
|
17010
|
+
}
|
|
17011
|
+
return picked;
|
|
17012
|
+
}
|
|
17013
|
+
function effectFor(pattern, chain, options) {
|
|
17014
|
+
const picked = [
|
|
17015
|
+
...new Set([chain.last, ...chain.between].flatMap(selectorOf))
|
|
17016
|
+
];
|
|
17017
|
+
return {
|
|
17018
|
+
type: "interaction",
|
|
17019
|
+
binding: storageRelationalBinding({
|
|
17020
|
+
recognition: "python-storage",
|
|
17021
|
+
storageSystem: pattern.storageSystem,
|
|
17022
|
+
scope: pattern.module,
|
|
17023
|
+
table: chain.subject
|
|
17024
|
+
}),
|
|
17025
|
+
callee: chain.last.text,
|
|
17026
|
+
interaction: {
|
|
17027
|
+
class: "storage-access",
|
|
17028
|
+
kind: pattern.writes.includes(chain.operation) ? "write" : "read",
|
|
17029
|
+
fields: fieldsOf(chain),
|
|
17030
|
+
operation: chain.operation,
|
|
17031
|
+
...picked.length > 0 ? { selector: picked } : {}
|
|
17032
|
+
}
|
|
17033
|
+
};
|
|
17034
|
+
}
|
|
17035
|
+
function children3(node) {
|
|
17036
|
+
return node.namedChildren.filter((child) => child !== null);
|
|
17037
|
+
}
|
|
16949
17038
|
function fileOf(key) {
|
|
16950
17039
|
const at = key.lastIndexOf(":");
|
|
16951
17040
|
return at === -1 ? key : key.slice(0, at);
|
|
@@ -16960,26 +17049,90 @@ function returnTypeName(node) {
|
|
|
16960
17049
|
}
|
|
16961
17050
|
return annotation.type === "subscript" ? field(annotation, "value")?.text ?? null : annotation.text;
|
|
16962
17051
|
}
|
|
17052
|
+
function callsIn(node, found = []) {
|
|
17053
|
+
for (const child of node.namedChildren) {
|
|
17054
|
+
if (child === null) {
|
|
17055
|
+
continue;
|
|
17056
|
+
}
|
|
17057
|
+
if (child.type === "call") {
|
|
17058
|
+
found.push(child);
|
|
17059
|
+
}
|
|
17060
|
+
callsIn(child, found);
|
|
17061
|
+
}
|
|
17062
|
+
return found;
|
|
17063
|
+
}
|
|
17064
|
+
function storageEffects(calls, options) {
|
|
17065
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17066
|
+
const asked = new Set(
|
|
17067
|
+
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
17068
|
+
);
|
|
17069
|
+
const gather = (inCalls, filePath) => {
|
|
17070
|
+
const found = [...directStorageEffects(inCalls, { ...options, filePath })];
|
|
17071
|
+
resolveCalls(
|
|
17072
|
+
options.facts,
|
|
17073
|
+
inCalls.filter((call) => options.leadsToStorage.has(methodName(call))).map((call) => field(call, "function")).filter((callee) => callee !== null).map((callee) => calleeKeyOf(filePath, callee)).filter((key) => {
|
|
17074
|
+
const fresh = !asked.has(key);
|
|
17075
|
+
asked.add(key);
|
|
17076
|
+
return fresh;
|
|
17077
|
+
})
|
|
17078
|
+
);
|
|
17079
|
+
for (const call of inCalls) {
|
|
17080
|
+
const callee = field(call, "function");
|
|
17081
|
+
if (callee === null || !options.leadsToStorage.has(methodName(call))) {
|
|
17082
|
+
continue;
|
|
17083
|
+
}
|
|
17084
|
+
const key = resolvedOnce(options, calleeKeyOf(filePath, callee));
|
|
17085
|
+
const body = key === void 0 ? void 0 : options.definitionAt(key);
|
|
17086
|
+
if (key === void 0 || body === void 0 || seen.has(key)) {
|
|
17087
|
+
continue;
|
|
17088
|
+
}
|
|
17089
|
+
seen.add(key);
|
|
17090
|
+
const name = field(body, "name")?.text;
|
|
17091
|
+
const origin = {
|
|
17092
|
+
file: fileOf(key),
|
|
17093
|
+
line: body.startPosition.row + 1,
|
|
17094
|
+
...name === void 0 ? {} : { function: name }
|
|
17095
|
+
};
|
|
17096
|
+
found.push(
|
|
17097
|
+
...gather(callsIn(body), fileOf(key)).map(
|
|
17098
|
+
(effect) => effect.type === "interaction" && effect.origin === void 0 ? { ...effect, origin } : effect
|
|
17099
|
+
)
|
|
17100
|
+
);
|
|
17101
|
+
}
|
|
17102
|
+
return found;
|
|
17103
|
+
};
|
|
17104
|
+
return gather(calls, options.filePath);
|
|
17105
|
+
}
|
|
17106
|
+
function calleeKeyOf(filePath, callee) {
|
|
17107
|
+
return callee.type === "identifier" ? `${filePath}#${callee.text}` : nodeId(filePath, callee);
|
|
17108
|
+
}
|
|
17109
|
+
function resolvedOnce(options, calleeKey) {
|
|
17110
|
+
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === calleeKey).map((row) => String(row[1]));
|
|
17111
|
+
return resolved.length === 1 ? resolved[0] : void 0;
|
|
17112
|
+
}
|
|
16963
17113
|
function storageCallees(calls, filePath, couldMatch) {
|
|
16964
17114
|
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
17115
|
}
|
|
16966
|
-
function
|
|
17116
|
+
function directStorageEffects(calls, options) {
|
|
16967
17117
|
if (options.patterns.length === 0) {
|
|
16968
17118
|
return [];
|
|
16969
17119
|
}
|
|
17120
|
+
const named = new Set(
|
|
17121
|
+
options.patterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
17122
|
+
);
|
|
16970
17123
|
const chains = chainsIn(calls).filter(
|
|
16971
|
-
(chain) => options.couldMatch.has(methodName(chain.root))
|
|
17124
|
+
(chain) => options.couldMatch.has(methodName(chain.root)) || named.has(methodName(chain.root))
|
|
16972
17125
|
);
|
|
16973
17126
|
if (chains.length === 0) {
|
|
16974
17127
|
return [];
|
|
16975
17128
|
}
|
|
16976
|
-
const
|
|
17129
|
+
const alreadyAsked = new Set(
|
|
16977
17130
|
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
16978
17131
|
);
|
|
16979
17132
|
resolveCalls(
|
|
16980
17133
|
options.facts,
|
|
16981
17134
|
storageCallees(calls, options.filePath, options.couldMatch).filter(
|
|
16982
|
-
(key) => !
|
|
17135
|
+
(key) => !alreadyAsked.has(key)
|
|
16983
17136
|
)
|
|
16984
17137
|
);
|
|
16985
17138
|
const effects = [];
|
|
@@ -16988,6 +17141,11 @@ function storageEffects(calls, options) {
|
|
|
16988
17141
|
if (callee === null) {
|
|
16989
17142
|
continue;
|
|
16990
17143
|
}
|
|
17144
|
+
const imported = importedQueryFunction(options, chain);
|
|
17145
|
+
if (imported !== void 0) {
|
|
17146
|
+
effects.push(effectFor(imported, chain, options));
|
|
17147
|
+
continue;
|
|
17148
|
+
}
|
|
16991
17149
|
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === nodeId(options.filePath, callee)).map((row) => String(row[1]));
|
|
16992
17150
|
const settled = resolved.length === 1 ? resolved[0] : void 0;
|
|
16993
17151
|
if (settled === void 0) {
|
|
@@ -17005,22 +17163,7 @@ function storageEffects(calls, options) {
|
|
|
17005
17163
|
if (pattern === void 0) {
|
|
17006
17164
|
continue;
|
|
17007
17165
|
}
|
|
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
|
-
});
|
|
17166
|
+
effects.push(effectFor(pattern, chain, options));
|
|
17024
17167
|
}
|
|
17025
17168
|
return effects;
|
|
17026
17169
|
}
|
|
@@ -17597,7 +17740,7 @@ function buildRouteUnit(options) {
|
|
|
17597
17740
|
const branches = (perReturn ?? []).map(
|
|
17598
17741
|
(branch) => storage.length === 0 ? branch : { ...branch, extraEffects: storage }
|
|
17599
17742
|
);
|
|
17600
|
-
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0)) {
|
|
17743
|
+
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0 || effects.length > 0)) {
|
|
17601
17744
|
branches.push({
|
|
17602
17745
|
conditions: [],
|
|
17603
17746
|
terminal: {
|
|
@@ -17897,6 +18040,11 @@ function emitModuleImportFacts(db, filePath, module2, resolverOptions) {
|
|
|
17897
18040
|
moduleText,
|
|
17898
18041
|
resolution.status === "resolved" ? "resolved" : resolution.reason
|
|
17899
18042
|
]);
|
|
18043
|
+
db.add("pyImportedName", [
|
|
18044
|
+
`${filePath}#${localName}`,
|
|
18045
|
+
moduleText,
|
|
18046
|
+
binding.kind === "import" ? binding.localName : binding.importedName
|
|
18047
|
+
]);
|
|
17900
18048
|
if (resolution.status === "resolved") {
|
|
17901
18049
|
db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
|
|
17902
18050
|
db.add("imports", [
|
|
@@ -18047,6 +18195,11 @@ function unmountedReason(index) {
|
|
|
18047
18195
|
if (loops.length === 0) {
|
|
18048
18196
|
return "is never mounted through a single variable binding in the files read";
|
|
18049
18197
|
}
|
|
18198
|
+
const stopped = loops.find((loop) => loop.unmatched !== void 0);
|
|
18199
|
+
if (stopped?.unmatched !== void 0) {
|
|
18200
|
+
const { name, resolvedTo, matched, of } = stopped.unmatched;
|
|
18201
|
+
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`;
|
|
18202
|
+
}
|
|
18050
18203
|
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
18204
|
}
|
|
18052
18205
|
function rivalRegistration(index, site) {
|
|
@@ -18474,25 +18627,32 @@ function mountCallOf(stmt, scope, scan) {
|
|
|
18474
18627
|
function constructionsReturnedBy(target, index) {
|
|
18475
18628
|
const facts = index.facts;
|
|
18476
18629
|
if (facts === void 0) {
|
|
18477
|
-
return
|
|
18630
|
+
return { kind: "unread" };
|
|
18478
18631
|
}
|
|
18479
18632
|
const callKey = nodeId(target.file, target.call);
|
|
18480
18633
|
resolveCalls(facts, [callKey]);
|
|
18481
18634
|
const returned = objectReturnedBy(facts, callKey);
|
|
18482
18635
|
if (returned === null) {
|
|
18483
|
-
return
|
|
18636
|
+
return { kind: "unread" };
|
|
18484
18637
|
}
|
|
18485
18638
|
const contained = containedValues(facts, returned);
|
|
18486
18639
|
resolveCalls(facts, contained);
|
|
18487
18640
|
const found = [];
|
|
18488
18641
|
for (const value of contained) {
|
|
18489
|
-
const
|
|
18642
|
+
const resolved = resolvedValueKey(facts, value);
|
|
18643
|
+
const construction = index.byValueKey.get(resolved);
|
|
18490
18644
|
if (construction === void 0) {
|
|
18491
|
-
return
|
|
18645
|
+
return {
|
|
18646
|
+
kind: "unmatched",
|
|
18647
|
+
name: value,
|
|
18648
|
+
resolvedTo: resolved,
|
|
18649
|
+
matched: found.length,
|
|
18650
|
+
of: contained.length
|
|
18651
|
+
};
|
|
18492
18652
|
}
|
|
18493
18653
|
found.push(construction);
|
|
18494
18654
|
}
|
|
18495
|
-
return found;
|
|
18655
|
+
return { kind: "settled", found };
|
|
18496
18656
|
}
|
|
18497
18657
|
function resolvedValueKey(facts, value) {
|
|
18498
18658
|
const row = facts.facts("wantedIsWrittenAs").find((entry) => String(entry[0]) === value);
|
|
@@ -18514,10 +18674,16 @@ function mountedConstructions(arg, position, scan) {
|
|
|
18514
18674
|
return target.elements.flatMap(named);
|
|
18515
18675
|
}
|
|
18516
18676
|
if (target.kind === "call") {
|
|
18517
|
-
const
|
|
18518
|
-
if (settled.length > 0) {
|
|
18519
|
-
return
|
|
18520
|
-
}
|
|
18677
|
+
const returned = constructionsReturnedBy(target, scan.index);
|
|
18678
|
+
if (returned.kind === "settled" && returned.found.length > 0) {
|
|
18679
|
+
return returned.found;
|
|
18680
|
+
}
|
|
18681
|
+
scan.index.unenumerableLoops.set(target.location, {
|
|
18682
|
+
site: position.site,
|
|
18683
|
+
location: target.location,
|
|
18684
|
+
...returned.kind === "unmatched" ? { unmatched: returned } : {}
|
|
18685
|
+
});
|
|
18686
|
+
return [];
|
|
18521
18687
|
}
|
|
18522
18688
|
scan.index.unenumerableLoops.set(target.location, {
|
|
18523
18689
|
site: position.site,
|
|
@@ -18835,6 +19001,11 @@ async function extractPythonProject(options) {
|
|
|
18835
19001
|
}
|
|
18836
19002
|
}
|
|
18837
19003
|
const couldMatch = methodsDeclaredNear(db, storagePatterns, definitions);
|
|
19004
|
+
const startsAQuery = /* @__PURE__ */ new Set([
|
|
19005
|
+
...couldMatch,
|
|
19006
|
+
...storagePatterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
19007
|
+
]);
|
|
19008
|
+
const leadsToStorage = functionsReachingStorage(definitions, startsAQuery);
|
|
18838
19009
|
const routerIndex = buildRouterIndex(bound, options.packs, {
|
|
18839
19010
|
roots: options.roots,
|
|
18840
19011
|
...mountsRouters ? { facts: db } : {}
|
|
@@ -18852,7 +19023,8 @@ async function extractPythonProject(options) {
|
|
|
18852
19023
|
factsPath: file2,
|
|
18853
19024
|
patterns: storagePatterns,
|
|
18854
19025
|
definitionAt: (key) => definitions.get(key),
|
|
18855
|
-
couldMatch
|
|
19026
|
+
couldMatch,
|
|
19027
|
+
leadsToStorage
|
|
18856
19028
|
}
|
|
18857
19029
|
} : {}
|
|
18858
19030
|
});
|
|
@@ -18918,6 +19090,52 @@ function methodsDeclaredNear(db, patterns, definitions) {
|
|
|
18918
19090
|
}
|
|
18919
19091
|
return found;
|
|
18920
19092
|
}
|
|
19093
|
+
function calledName(call) {
|
|
19094
|
+
const callee = field(call, "function");
|
|
19095
|
+
if (callee === null) {
|
|
19096
|
+
return "";
|
|
19097
|
+
}
|
|
19098
|
+
return callee.type === "attribute" ? field(callee, "attribute")?.text ?? "" : callee.text;
|
|
19099
|
+
}
|
|
19100
|
+
function callsUnder(node, found = []) {
|
|
19101
|
+
for (const child of node.namedChildren) {
|
|
19102
|
+
if (child === null) {
|
|
19103
|
+
continue;
|
|
19104
|
+
}
|
|
19105
|
+
if (child.type === "call") {
|
|
19106
|
+
found.push(child);
|
|
19107
|
+
}
|
|
19108
|
+
callsUnder(child, found);
|
|
19109
|
+
}
|
|
19110
|
+
return found;
|
|
19111
|
+
}
|
|
19112
|
+
function functionsReachingStorage(definitions, couldMatch) {
|
|
19113
|
+
const bodies = [];
|
|
19114
|
+
for (const node of definitions.values()) {
|
|
19115
|
+
const name = field(node, "name");
|
|
19116
|
+
if (name === null) {
|
|
19117
|
+
continue;
|
|
19118
|
+
}
|
|
19119
|
+
bodies.push({ name: name.text, calls: callsUnder(node).map(calledName) });
|
|
19120
|
+
}
|
|
19121
|
+
const reaching = /* @__PURE__ */ new Set();
|
|
19122
|
+
for (const body of bodies) {
|
|
19123
|
+
if (body.calls.some((called) => couldMatch.has(called))) {
|
|
19124
|
+
reaching.add(body.name);
|
|
19125
|
+
}
|
|
19126
|
+
}
|
|
19127
|
+
for (let grew = true; grew; ) {
|
|
19128
|
+
grew = false;
|
|
19129
|
+
for (const body of bodies) {
|
|
19130
|
+
if (reaching.has(body.name) || !body.calls.some((c) => reaching.has(c))) {
|
|
19131
|
+
continue;
|
|
19132
|
+
}
|
|
19133
|
+
reaching.add(body.name);
|
|
19134
|
+
grew = true;
|
|
19135
|
+
}
|
|
19136
|
+
}
|
|
19137
|
+
return reaching;
|
|
19138
|
+
}
|
|
18921
19139
|
// Annotate the CommonJS export names for ESM import in node:
|
|
18922
19140
|
0 && (module.exports = {
|
|
18923
19141
|
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
|
@@ -950,12 +950,26 @@ function argsOf(call) {
|
|
|
950
950
|
function calleeText(call) {
|
|
951
951
|
return field(call, "function")?.text ?? call.text;
|
|
952
952
|
}
|
|
953
|
+
function withoutChainLinks(calls) {
|
|
954
|
+
const isLink = /* @__PURE__ */ new Set();
|
|
955
|
+
for (const call of calls) {
|
|
956
|
+
const callee = field(call, "function");
|
|
957
|
+
if (callee === null || callee.type !== "attribute") {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const object2 = field(callee, "object");
|
|
961
|
+
if (object2 !== null && object2.type === "call") {
|
|
962
|
+
isLink.add(object2.id);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return calls.filter((call) => !isLink.has(call.id));
|
|
966
|
+
}
|
|
953
967
|
function invocationEffects(definitionNode) {
|
|
954
968
|
const body = field(definitionNode, "body");
|
|
955
969
|
if (body === null) {
|
|
956
970
|
return [];
|
|
957
971
|
}
|
|
958
|
-
const calls = bodyCalls(body);
|
|
972
|
+
const calls = withoutChainLinks(bodyCalls(body));
|
|
959
973
|
if (calls.length === 0) {
|
|
960
974
|
return [];
|
|
961
975
|
}
|
|
@@ -16189,21 +16203,22 @@ var RESOLUTION_RULES = [
|
|
|
16189
16203
|
]
|
|
16190
16204
|
),
|
|
16191
16205
|
// 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
|
-
//
|
|
16206
|
+
// rather than by resolving every callee in the project. A caller knows
|
|
16207
|
+
// the function and wants its call sites, so both of these start from
|
|
16208
|
+
// the function. Starting from `call` instead asks what every call in
|
|
16209
|
+
// the project imports, which was 72% of everything derived.
|
|
16195
16210
|
rule(
|
|
16196
16211
|
"callsFunction",
|
|
16197
16212
|
[v("r"), v("f")],
|
|
16198
|
-
[lit("
|
|
16213
|
+
[lit("binds", v("c"), v("f")), lit("call", v("r"), v("c"))]
|
|
16199
16214
|
),
|
|
16200
16215
|
rule(
|
|
16201
16216
|
"callsFunction",
|
|
16202
16217
|
[v("r"), v("f")],
|
|
16203
16218
|
[
|
|
16204
|
-
lit("
|
|
16219
|
+
lit("moduleExport", v("m"), v("n"), v("f")),
|
|
16205
16220
|
lit("imports", v("c"), v("m"), v("n")),
|
|
16206
|
-
lit("
|
|
16221
|
+
lit("call", v("r"), v("c"))
|
|
16207
16222
|
]
|
|
16208
16223
|
),
|
|
16209
16224
|
// A function whose return is itself a call arrives at the same object
|
|
@@ -16897,11 +16912,85 @@ function chainsIn(calls) {
|
|
|
16897
16912
|
root: call,
|
|
16898
16913
|
last,
|
|
16899
16914
|
subject: subjectOf(call),
|
|
16900
|
-
operation: methodName(last)
|
|
16915
|
+
operation: methodName(last),
|
|
16916
|
+
between: calls.filter(
|
|
16917
|
+
(other) => other !== call && other !== last && other.startIndex >= call.startIndex && other.endIndex <= last.endIndex
|
|
16918
|
+
)
|
|
16901
16919
|
});
|
|
16902
16920
|
}
|
|
16903
16921
|
return chains;
|
|
16904
16922
|
}
|
|
16923
|
+
function importedQueryFunction(options, chain) {
|
|
16924
|
+
const callee = field(chain.root, "function");
|
|
16925
|
+
if (callee === null || callee.type !== "identifier") {
|
|
16926
|
+
return void 0;
|
|
16927
|
+
}
|
|
16928
|
+
const from = options.facts.facts("pyImportedName").find((row) => String(row[0]) === `${options.filePath}#${callee.text}`);
|
|
16929
|
+
if (from === void 0) {
|
|
16930
|
+
return void 0;
|
|
16931
|
+
}
|
|
16932
|
+
return options.patterns.find(
|
|
16933
|
+
(pattern) => String(from[1]) === pattern.module && (pattern.queryFunctions ?? []).includes(String(from[2]))
|
|
16934
|
+
);
|
|
16935
|
+
}
|
|
16936
|
+
function fieldsOf(chain) {
|
|
16937
|
+
const args = field(chain.root, "arguments");
|
|
16938
|
+
const named = [];
|
|
16939
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
16940
|
+
if (argument.type === "keyword_argument") {
|
|
16941
|
+
const name = field(argument, "name");
|
|
16942
|
+
if (name !== null) {
|
|
16943
|
+
named.push(name.text);
|
|
16944
|
+
}
|
|
16945
|
+
continue;
|
|
16946
|
+
}
|
|
16947
|
+
if (argument.type === "attribute") {
|
|
16948
|
+
const property = field(argument, "attribute");
|
|
16949
|
+
if (property !== null) {
|
|
16950
|
+
named.push(property.text);
|
|
16951
|
+
}
|
|
16952
|
+
}
|
|
16953
|
+
}
|
|
16954
|
+
return named;
|
|
16955
|
+
}
|
|
16956
|
+
function selectorOf(call) {
|
|
16957
|
+
const args = field(call, "arguments");
|
|
16958
|
+
const picked = [];
|
|
16959
|
+
for (const argument of args === null ? [] : children3(args)) {
|
|
16960
|
+
if (argument.type === "keyword_argument") {
|
|
16961
|
+
const name = field(argument, "name");
|
|
16962
|
+
if (name !== null) {
|
|
16963
|
+
picked.push(name.text);
|
|
16964
|
+
}
|
|
16965
|
+
}
|
|
16966
|
+
}
|
|
16967
|
+
return picked;
|
|
16968
|
+
}
|
|
16969
|
+
function effectFor(pattern, chain, options) {
|
|
16970
|
+
const picked = [
|
|
16971
|
+
...new Set([chain.last, ...chain.between].flatMap(selectorOf))
|
|
16972
|
+
];
|
|
16973
|
+
return {
|
|
16974
|
+
type: "interaction",
|
|
16975
|
+
binding: storageRelationalBinding({
|
|
16976
|
+
recognition: "python-storage",
|
|
16977
|
+
storageSystem: pattern.storageSystem,
|
|
16978
|
+
scope: pattern.module,
|
|
16979
|
+
table: chain.subject
|
|
16980
|
+
}),
|
|
16981
|
+
callee: chain.last.text,
|
|
16982
|
+
interaction: {
|
|
16983
|
+
class: "storage-access",
|
|
16984
|
+
kind: pattern.writes.includes(chain.operation) ? "write" : "read",
|
|
16985
|
+
fields: fieldsOf(chain),
|
|
16986
|
+
operation: chain.operation,
|
|
16987
|
+
...picked.length > 0 ? { selector: picked } : {}
|
|
16988
|
+
}
|
|
16989
|
+
};
|
|
16990
|
+
}
|
|
16991
|
+
function children3(node) {
|
|
16992
|
+
return node.namedChildren.filter((child) => child !== null);
|
|
16993
|
+
}
|
|
16905
16994
|
function fileOf(key) {
|
|
16906
16995
|
const at = key.lastIndexOf(":");
|
|
16907
16996
|
return at === -1 ? key : key.slice(0, at);
|
|
@@ -16916,26 +17005,90 @@ function returnTypeName(node) {
|
|
|
16916
17005
|
}
|
|
16917
17006
|
return annotation.type === "subscript" ? field(annotation, "value")?.text ?? null : annotation.text;
|
|
16918
17007
|
}
|
|
17008
|
+
function callsIn(node, found = []) {
|
|
17009
|
+
for (const child of node.namedChildren) {
|
|
17010
|
+
if (child === null) {
|
|
17011
|
+
continue;
|
|
17012
|
+
}
|
|
17013
|
+
if (child.type === "call") {
|
|
17014
|
+
found.push(child);
|
|
17015
|
+
}
|
|
17016
|
+
callsIn(child, found);
|
|
17017
|
+
}
|
|
17018
|
+
return found;
|
|
17019
|
+
}
|
|
17020
|
+
function storageEffects(calls, options) {
|
|
17021
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17022
|
+
const asked = new Set(
|
|
17023
|
+
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
17024
|
+
);
|
|
17025
|
+
const gather = (inCalls, filePath) => {
|
|
17026
|
+
const found = [...directStorageEffects(inCalls, { ...options, filePath })];
|
|
17027
|
+
resolveCalls(
|
|
17028
|
+
options.facts,
|
|
17029
|
+
inCalls.filter((call) => options.leadsToStorage.has(methodName(call))).map((call) => field(call, "function")).filter((callee) => callee !== null).map((callee) => calleeKeyOf(filePath, callee)).filter((key) => {
|
|
17030
|
+
const fresh = !asked.has(key);
|
|
17031
|
+
asked.add(key);
|
|
17032
|
+
return fresh;
|
|
17033
|
+
})
|
|
17034
|
+
);
|
|
17035
|
+
for (const call of inCalls) {
|
|
17036
|
+
const callee = field(call, "function");
|
|
17037
|
+
if (callee === null || !options.leadsToStorage.has(methodName(call))) {
|
|
17038
|
+
continue;
|
|
17039
|
+
}
|
|
17040
|
+
const key = resolvedOnce(options, calleeKeyOf(filePath, callee));
|
|
17041
|
+
const body = key === void 0 ? void 0 : options.definitionAt(key);
|
|
17042
|
+
if (key === void 0 || body === void 0 || seen.has(key)) {
|
|
17043
|
+
continue;
|
|
17044
|
+
}
|
|
17045
|
+
seen.add(key);
|
|
17046
|
+
const name = field(body, "name")?.text;
|
|
17047
|
+
const origin = {
|
|
17048
|
+
file: fileOf(key),
|
|
17049
|
+
line: body.startPosition.row + 1,
|
|
17050
|
+
...name === void 0 ? {} : { function: name }
|
|
17051
|
+
};
|
|
17052
|
+
found.push(
|
|
17053
|
+
...gather(callsIn(body), fileOf(key)).map(
|
|
17054
|
+
(effect) => effect.type === "interaction" && effect.origin === void 0 ? { ...effect, origin } : effect
|
|
17055
|
+
)
|
|
17056
|
+
);
|
|
17057
|
+
}
|
|
17058
|
+
return found;
|
|
17059
|
+
};
|
|
17060
|
+
return gather(calls, options.filePath);
|
|
17061
|
+
}
|
|
17062
|
+
function calleeKeyOf(filePath, callee) {
|
|
17063
|
+
return callee.type === "identifier" ? `${filePath}#${callee.text}` : nodeId(filePath, callee);
|
|
17064
|
+
}
|
|
17065
|
+
function resolvedOnce(options, calleeKey) {
|
|
17066
|
+
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === calleeKey).map((row) => String(row[1]));
|
|
17067
|
+
return resolved.length === 1 ? resolved[0] : void 0;
|
|
17068
|
+
}
|
|
16919
17069
|
function storageCallees(calls, filePath, couldMatch) {
|
|
16920
17070
|
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
17071
|
}
|
|
16922
|
-
function
|
|
17072
|
+
function directStorageEffects(calls, options) {
|
|
16923
17073
|
if (options.patterns.length === 0) {
|
|
16924
17074
|
return [];
|
|
16925
17075
|
}
|
|
17076
|
+
const named = new Set(
|
|
17077
|
+
options.patterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
17078
|
+
);
|
|
16926
17079
|
const chains = chainsIn(calls).filter(
|
|
16927
|
-
(chain) => options.couldMatch.has(methodName(chain.root))
|
|
17080
|
+
(chain) => options.couldMatch.has(methodName(chain.root)) || named.has(methodName(chain.root))
|
|
16928
17081
|
);
|
|
16929
17082
|
if (chains.length === 0) {
|
|
16930
17083
|
return [];
|
|
16931
17084
|
}
|
|
16932
|
-
const
|
|
17085
|
+
const alreadyAsked = new Set(
|
|
16933
17086
|
options.facts.facts("wanted").map((row) => String(row[0]))
|
|
16934
17087
|
);
|
|
16935
17088
|
resolveCalls(
|
|
16936
17089
|
options.facts,
|
|
16937
17090
|
storageCallees(calls, options.filePath, options.couldMatch).filter(
|
|
16938
|
-
(key) => !
|
|
17091
|
+
(key) => !alreadyAsked.has(key)
|
|
16939
17092
|
)
|
|
16940
17093
|
);
|
|
16941
17094
|
const effects = [];
|
|
@@ -16944,6 +17097,11 @@ function storageEffects(calls, options) {
|
|
|
16944
17097
|
if (callee === null) {
|
|
16945
17098
|
continue;
|
|
16946
17099
|
}
|
|
17100
|
+
const imported = importedQueryFunction(options, chain);
|
|
17101
|
+
if (imported !== void 0) {
|
|
17102
|
+
effects.push(effectFor(imported, chain, options));
|
|
17103
|
+
continue;
|
|
17104
|
+
}
|
|
16947
17105
|
const resolved = options.facts.facts("wantedResolves").filter((row) => String(row[0]) === nodeId(options.filePath, callee)).map((row) => String(row[1]));
|
|
16948
17106
|
const settled = resolved.length === 1 ? resolved[0] : void 0;
|
|
16949
17107
|
if (settled === void 0) {
|
|
@@ -16961,22 +17119,7 @@ function storageEffects(calls, options) {
|
|
|
16961
17119
|
if (pattern === void 0) {
|
|
16962
17120
|
continue;
|
|
16963
17121
|
}
|
|
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
|
-
});
|
|
17122
|
+
effects.push(effectFor(pattern, chain, options));
|
|
16980
17123
|
}
|
|
16981
17124
|
return effects;
|
|
16982
17125
|
}
|
|
@@ -17553,7 +17696,7 @@ function buildRouteUnit(options) {
|
|
|
17553
17696
|
const branches = (perReturn ?? []).map(
|
|
17554
17697
|
(branch) => storage.length === 0 ? branch : { ...branch, extraEffects: storage }
|
|
17555
17698
|
);
|
|
17556
|
-
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0)) {
|
|
17699
|
+
if (perReturn === null && (responseShape.reading.kind !== "absent" || statusCode.reading.kind !== "absent" || storage.length > 0 || effects.length > 0)) {
|
|
17557
17700
|
branches.push({
|
|
17558
17701
|
conditions: [],
|
|
17559
17702
|
terminal: {
|
|
@@ -17853,6 +17996,11 @@ function emitModuleImportFacts(db, filePath, module, resolverOptions) {
|
|
|
17853
17996
|
moduleText,
|
|
17854
17997
|
resolution.status === "resolved" ? "resolved" : resolution.reason
|
|
17855
17998
|
]);
|
|
17999
|
+
db.add("pyImportedName", [
|
|
18000
|
+
`${filePath}#${localName}`,
|
|
18001
|
+
moduleText,
|
|
18002
|
+
binding.kind === "import" ? binding.localName : binding.importedName
|
|
18003
|
+
]);
|
|
17856
18004
|
if (resolution.status === "resolved") {
|
|
17857
18005
|
db.add("pyImportResolved", [filePath, moduleText, resolution.file]);
|
|
17858
18006
|
db.add("imports", [
|
|
@@ -18003,6 +18151,11 @@ function unmountedReason(index) {
|
|
|
18003
18151
|
if (loops.length === 0) {
|
|
18004
18152
|
return "is never mounted through a single variable binding in the files read";
|
|
18005
18153
|
}
|
|
18154
|
+
const stopped = loops.find((loop) => loop.unmatched !== void 0);
|
|
18155
|
+
if (stopped?.unmatched !== void 0) {
|
|
18156
|
+
const { name, resolvedTo, matched, of } = stopped.unmatched;
|
|
18157
|
+
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`;
|
|
18158
|
+
}
|
|
18006
18159
|
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
18160
|
}
|
|
18008
18161
|
function rivalRegistration(index, site) {
|
|
@@ -18430,25 +18583,32 @@ function mountCallOf(stmt, scope, scan) {
|
|
|
18430
18583
|
function constructionsReturnedBy(target, index) {
|
|
18431
18584
|
const facts = index.facts;
|
|
18432
18585
|
if (facts === void 0) {
|
|
18433
|
-
return
|
|
18586
|
+
return { kind: "unread" };
|
|
18434
18587
|
}
|
|
18435
18588
|
const callKey = nodeId(target.file, target.call);
|
|
18436
18589
|
resolveCalls(facts, [callKey]);
|
|
18437
18590
|
const returned = objectReturnedBy(facts, callKey);
|
|
18438
18591
|
if (returned === null) {
|
|
18439
|
-
return
|
|
18592
|
+
return { kind: "unread" };
|
|
18440
18593
|
}
|
|
18441
18594
|
const contained = containedValues(facts, returned);
|
|
18442
18595
|
resolveCalls(facts, contained);
|
|
18443
18596
|
const found = [];
|
|
18444
18597
|
for (const value of contained) {
|
|
18445
|
-
const
|
|
18598
|
+
const resolved = resolvedValueKey(facts, value);
|
|
18599
|
+
const construction = index.byValueKey.get(resolved);
|
|
18446
18600
|
if (construction === void 0) {
|
|
18447
|
-
return
|
|
18601
|
+
return {
|
|
18602
|
+
kind: "unmatched",
|
|
18603
|
+
name: value,
|
|
18604
|
+
resolvedTo: resolved,
|
|
18605
|
+
matched: found.length,
|
|
18606
|
+
of: contained.length
|
|
18607
|
+
};
|
|
18448
18608
|
}
|
|
18449
18609
|
found.push(construction);
|
|
18450
18610
|
}
|
|
18451
|
-
return found;
|
|
18611
|
+
return { kind: "settled", found };
|
|
18452
18612
|
}
|
|
18453
18613
|
function resolvedValueKey(facts, value) {
|
|
18454
18614
|
const row = facts.facts("wantedIsWrittenAs").find((entry) => String(entry[0]) === value);
|
|
@@ -18470,10 +18630,16 @@ function mountedConstructions(arg, position, scan) {
|
|
|
18470
18630
|
return target.elements.flatMap(named);
|
|
18471
18631
|
}
|
|
18472
18632
|
if (target.kind === "call") {
|
|
18473
|
-
const
|
|
18474
|
-
if (settled.length > 0) {
|
|
18475
|
-
return
|
|
18476
|
-
}
|
|
18633
|
+
const returned = constructionsReturnedBy(target, scan.index);
|
|
18634
|
+
if (returned.kind === "settled" && returned.found.length > 0) {
|
|
18635
|
+
return returned.found;
|
|
18636
|
+
}
|
|
18637
|
+
scan.index.unenumerableLoops.set(target.location, {
|
|
18638
|
+
site: position.site,
|
|
18639
|
+
location: target.location,
|
|
18640
|
+
...returned.kind === "unmatched" ? { unmatched: returned } : {}
|
|
18641
|
+
});
|
|
18642
|
+
return [];
|
|
18477
18643
|
}
|
|
18478
18644
|
scan.index.unenumerableLoops.set(target.location, {
|
|
18479
18645
|
site: position.site,
|
|
@@ -18791,6 +18957,11 @@ async function extractPythonProject(options) {
|
|
|
18791
18957
|
}
|
|
18792
18958
|
}
|
|
18793
18959
|
const couldMatch = methodsDeclaredNear(db, storagePatterns, definitions);
|
|
18960
|
+
const startsAQuery = /* @__PURE__ */ new Set([
|
|
18961
|
+
...couldMatch,
|
|
18962
|
+
...storagePatterns.flatMap((pattern) => pattern.queryFunctions ?? [])
|
|
18963
|
+
]);
|
|
18964
|
+
const leadsToStorage = functionsReachingStorage(definitions, startsAQuery);
|
|
18794
18965
|
const routerIndex = buildRouterIndex(bound, options.packs, {
|
|
18795
18966
|
roots: options.roots,
|
|
18796
18967
|
...mountsRouters ? { facts: db } : {}
|
|
@@ -18808,7 +18979,8 @@ async function extractPythonProject(options) {
|
|
|
18808
18979
|
factsPath: file2,
|
|
18809
18980
|
patterns: storagePatterns,
|
|
18810
18981
|
definitionAt: (key) => definitions.get(key),
|
|
18811
|
-
couldMatch
|
|
18982
|
+
couldMatch,
|
|
18983
|
+
leadsToStorage
|
|
18812
18984
|
}
|
|
18813
18985
|
} : {}
|
|
18814
18986
|
});
|
|
@@ -18874,6 +19046,52 @@ function methodsDeclaredNear(db, patterns, definitions) {
|
|
|
18874
19046
|
}
|
|
18875
19047
|
return found;
|
|
18876
19048
|
}
|
|
19049
|
+
function calledName(call) {
|
|
19050
|
+
const callee = field(call, "function");
|
|
19051
|
+
if (callee === null) {
|
|
19052
|
+
return "";
|
|
19053
|
+
}
|
|
19054
|
+
return callee.type === "attribute" ? field(callee, "attribute")?.text ?? "" : callee.text;
|
|
19055
|
+
}
|
|
19056
|
+
function callsUnder(node, found = []) {
|
|
19057
|
+
for (const child of node.namedChildren) {
|
|
19058
|
+
if (child === null) {
|
|
19059
|
+
continue;
|
|
19060
|
+
}
|
|
19061
|
+
if (child.type === "call") {
|
|
19062
|
+
found.push(child);
|
|
19063
|
+
}
|
|
19064
|
+
callsUnder(child, found);
|
|
19065
|
+
}
|
|
19066
|
+
return found;
|
|
19067
|
+
}
|
|
19068
|
+
function functionsReachingStorage(definitions, couldMatch) {
|
|
19069
|
+
const bodies = [];
|
|
19070
|
+
for (const node of definitions.values()) {
|
|
19071
|
+
const name = field(node, "name");
|
|
19072
|
+
if (name === null) {
|
|
19073
|
+
continue;
|
|
19074
|
+
}
|
|
19075
|
+
bodies.push({ name: name.text, calls: callsUnder(node).map(calledName) });
|
|
19076
|
+
}
|
|
19077
|
+
const reaching = /* @__PURE__ */ new Set();
|
|
19078
|
+
for (const body of bodies) {
|
|
19079
|
+
if (body.calls.some((called) => couldMatch.has(called))) {
|
|
19080
|
+
reaching.add(body.name);
|
|
19081
|
+
}
|
|
19082
|
+
}
|
|
19083
|
+
for (let grew = true; grew; ) {
|
|
19084
|
+
grew = false;
|
|
19085
|
+
for (const body of bodies) {
|
|
19086
|
+
if (reaching.has(body.name) || !body.calls.some((c) => reaching.has(c))) {
|
|
19087
|
+
continue;
|
|
19088
|
+
}
|
|
19089
|
+
reaching.add(body.name);
|
|
19090
|
+
grew = true;
|
|
19091
|
+
}
|
|
19092
|
+
}
|
|
19093
|
+
return reaching;
|
|
19094
|
+
}
|
|
18877
19095
|
export {
|
|
18878
19096
|
annotationToShape,
|
|
18879
19097
|
bindModule,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/adapter-python",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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.1",
|
|
51
|
+
"@suss/datalog": "0.5.1",
|
|
52
|
+
"@suss/extractor": "0.5.1",
|
|
53
53
|
"web-tree-sitter": "^0.26.11"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|