circle-ir 3.31.0 → 3.33.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/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +4 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +453 -7
- package/dist/core/extractors/index.d.ts +1 -0
- package/dist/core/extractors/index.d.ts.map +1 -1
- package/dist/core/extractors/index.js +1 -0
- package/dist/core/extractors/index.js.map +1 -1
- package/dist/core/extractors/runtime-registrations.d.ts +42 -0
- package/dist/core/extractors/runtime-registrations.d.ts.map +1 -0
- package/dist/core/extractors/runtime-registrations.js +555 -0
- package/dist/core/extractors/runtime-registrations.js.map +1 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/SPEC.md +2 -0
- package/package.json +1 -1
|
@@ -9605,6 +9605,448 @@ function findChildByTypeGo(node, type) {
|
|
|
9605
9605
|
return null;
|
|
9606
9606
|
}
|
|
9607
9607
|
|
|
9608
|
+
// src/core/extractors/runtime-registrations.ts
|
|
9609
|
+
var HTTP_VERB_METHODS = /* @__PURE__ */ new Set([
|
|
9610
|
+
"get",
|
|
9611
|
+
"post",
|
|
9612
|
+
"put",
|
|
9613
|
+
"patch",
|
|
9614
|
+
"delete",
|
|
9615
|
+
"head",
|
|
9616
|
+
"options",
|
|
9617
|
+
"all"
|
|
9618
|
+
]);
|
|
9619
|
+
var MIDDLEWARE_METHODS = /* @__PURE__ */ new Set(["use"]);
|
|
9620
|
+
var EVENT_LISTENER_METHODS = /* @__PURE__ */ new Set(["on", "once", "ws"]);
|
|
9621
|
+
var EXPRESS_RECEIVER_NAMES = /* @__PURE__ */ new Set([
|
|
9622
|
+
"app",
|
|
9623
|
+
"router",
|
|
9624
|
+
"server",
|
|
9625
|
+
"apiRouter",
|
|
9626
|
+
// common framework instances
|
|
9627
|
+
"fastify",
|
|
9628
|
+
"koa",
|
|
9629
|
+
"express"
|
|
9630
|
+
]);
|
|
9631
|
+
var FRAMEWORK_MODULE_PATTERNS = [
|
|
9632
|
+
/^express$/,
|
|
9633
|
+
/^@?fastify(\/.*)?$/,
|
|
9634
|
+
/^koa$/,
|
|
9635
|
+
/^restify$/,
|
|
9636
|
+
/^hapi$/,
|
|
9637
|
+
/^@nestjs\/common$/,
|
|
9638
|
+
/^@nestjs\/core$/
|
|
9639
|
+
];
|
|
9640
|
+
function extractRuntimeRegistrations(tree, cache, language, imports) {
|
|
9641
|
+
if (language === "javascript" || language === "typescript") {
|
|
9642
|
+
return extractJSRuntimeRegistrations(tree, cache, imports);
|
|
9643
|
+
}
|
|
9644
|
+
if (language === "python") {
|
|
9645
|
+
return extractPythonRuntimeRegistrations(tree, cache, imports);
|
|
9646
|
+
}
|
|
9647
|
+
return [];
|
|
9648
|
+
}
|
|
9649
|
+
function buildHandlerIndex(tree, cache, imports) {
|
|
9650
|
+
const decls = /* @__PURE__ */ new Map();
|
|
9651
|
+
const recordDeclaration = (name2, node) => {
|
|
9652
|
+
if (!decls.has(name2)) {
|
|
9653
|
+
decls.set(name2, {
|
|
9654
|
+
line: node.startPosition.row + 1,
|
|
9655
|
+
column: node.startPosition.column
|
|
9656
|
+
});
|
|
9657
|
+
}
|
|
9658
|
+
};
|
|
9659
|
+
for (const fn of getNodesFromCache(tree.rootNode, "function_declaration", cache)) {
|
|
9660
|
+
const nameNode = fn.childForFieldName("name");
|
|
9661
|
+
if (nameNode) recordDeclaration(getNodeText(nameNode), fn);
|
|
9662
|
+
}
|
|
9663
|
+
const collectVarDeclarators = (parentType) => {
|
|
9664
|
+
for (const decl of getNodesFromCache(tree.rootNode, parentType, cache)) {
|
|
9665
|
+
for (let i2 = 0; i2 < decl.childCount; i2++) {
|
|
9666
|
+
const child = decl.child(i2);
|
|
9667
|
+
if (!child || child.type !== "variable_declarator") continue;
|
|
9668
|
+
const nameNode = child.childForFieldName("name");
|
|
9669
|
+
const valueNode = child.childForFieldName("value");
|
|
9670
|
+
if (!nameNode || !valueNode) continue;
|
|
9671
|
+
if (valueNode.type === "arrow_function" || valueNode.type === "function_expression" || valueNode.type === "function") {
|
|
9672
|
+
recordDeclaration(getNodeText(nameNode), child);
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
}
|
|
9676
|
+
};
|
|
9677
|
+
collectVarDeclarators("lexical_declaration");
|
|
9678
|
+
collectVarDeclarators("variable_declaration");
|
|
9679
|
+
let hasFrameworkImport = false;
|
|
9680
|
+
if (imports) {
|
|
9681
|
+
for (const imp of imports) {
|
|
9682
|
+
const mod = imp.from_package ?? "";
|
|
9683
|
+
if (mod && FRAMEWORK_MODULE_PATTERNS.some((re) => re.test(mod))) {
|
|
9684
|
+
hasFrameworkImport = true;
|
|
9685
|
+
break;
|
|
9686
|
+
}
|
|
9687
|
+
}
|
|
9688
|
+
}
|
|
9689
|
+
return { declarations: decls, hasFrameworkImport };
|
|
9690
|
+
}
|
|
9691
|
+
function extractJSRuntimeRegistrations(tree, cache, imports) {
|
|
9692
|
+
const out2 = [];
|
|
9693
|
+
const index = buildHandlerIndex(tree, cache, imports);
|
|
9694
|
+
const callExpressions = getNodesFromCache(tree.rootNode, "call_expression", cache);
|
|
9695
|
+
for (const call of callExpressions) {
|
|
9696
|
+
const fnNode = call.childForFieldName("function");
|
|
9697
|
+
if (!fnNode || fnNode.type !== "member_expression") continue;
|
|
9698
|
+
const objectNode = fnNode.childForFieldName("object");
|
|
9699
|
+
const propertyNode = fnNode.childForFieldName("property");
|
|
9700
|
+
if (!objectNode || !propertyNode) continue;
|
|
9701
|
+
const method = getNodeText(propertyNode);
|
|
9702
|
+
const receiver = getNodeText(objectNode);
|
|
9703
|
+
const kind = classifyMethod(method);
|
|
9704
|
+
if (!kind) continue;
|
|
9705
|
+
if (!isExpressShapedReceiver(receiver) && !index.hasFrameworkImport) {
|
|
9706
|
+
continue;
|
|
9707
|
+
}
|
|
9708
|
+
const argsNode = call.childForFieldName("arguments");
|
|
9709
|
+
if (!argsNode) continue;
|
|
9710
|
+
const argNodes = getRealArgs(argsNode);
|
|
9711
|
+
if (argNodes.length === 0) continue;
|
|
9712
|
+
let path;
|
|
9713
|
+
let handlerStart = 0;
|
|
9714
|
+
const first = argNodes[0];
|
|
9715
|
+
if (first.type === "string") {
|
|
9716
|
+
path = stripQuotes(getNodeText(first));
|
|
9717
|
+
handlerStart = 1;
|
|
9718
|
+
} else if (first.type === "template_string" && !hasTemplateSubstitution(first)) {
|
|
9719
|
+
path = stripBackticks(getNodeText(first));
|
|
9720
|
+
handlerStart = 1;
|
|
9721
|
+
}
|
|
9722
|
+
for (let i2 = handlerStart; i2 < argNodes.length; i2++) {
|
|
9723
|
+
const handlerNode = argNodes[i2];
|
|
9724
|
+
const handler = resolveHandler(handlerNode, index);
|
|
9725
|
+
if (!handler) continue;
|
|
9726
|
+
out2.push({
|
|
9727
|
+
kind,
|
|
9728
|
+
framework: inferFramework(receiver, index.hasFrameworkImport),
|
|
9729
|
+
registrar: {
|
|
9730
|
+
method,
|
|
9731
|
+
receiver,
|
|
9732
|
+
line: call.startPosition.row + 1,
|
|
9733
|
+
column: call.startPosition.column
|
|
9734
|
+
},
|
|
9735
|
+
...path !== void 0 ? { path } : {},
|
|
9736
|
+
handler
|
|
9737
|
+
});
|
|
9738
|
+
}
|
|
9739
|
+
}
|
|
9740
|
+
return out2;
|
|
9741
|
+
}
|
|
9742
|
+
function classifyMethod(method) {
|
|
9743
|
+
if (HTTP_VERB_METHODS.has(method)) return "http_route";
|
|
9744
|
+
if (MIDDLEWARE_METHODS.has(method)) return "middleware";
|
|
9745
|
+
if (EVENT_LISTENER_METHODS.has(method)) return "event_listener";
|
|
9746
|
+
return null;
|
|
9747
|
+
}
|
|
9748
|
+
function isExpressShapedReceiver(receiver) {
|
|
9749
|
+
if (EXPRESS_RECEIVER_NAMES.has(receiver)) return true;
|
|
9750
|
+
if (/(?:Router|App|Server)$/.test(receiver)) return true;
|
|
9751
|
+
return false;
|
|
9752
|
+
}
|
|
9753
|
+
function inferFramework(receiver, hasFrameworkImport) {
|
|
9754
|
+
if (receiver === "fastify") return "fastify";
|
|
9755
|
+
if (receiver === "koa") return "koa";
|
|
9756
|
+
if (receiver === "express") return "express";
|
|
9757
|
+
return hasFrameworkImport ? "express" : "unknown";
|
|
9758
|
+
}
|
|
9759
|
+
function getRealArgs(argsNode) {
|
|
9760
|
+
const out2 = [];
|
|
9761
|
+
for (let i2 = 0; i2 < argsNode.childCount; i2++) {
|
|
9762
|
+
const child = argsNode.child(i2);
|
|
9763
|
+
if (!child) continue;
|
|
9764
|
+
if (child.type === "(" || child.type === ")" || child.type === ",") continue;
|
|
9765
|
+
out2.push(child);
|
|
9766
|
+
}
|
|
9767
|
+
return out2;
|
|
9768
|
+
}
|
|
9769
|
+
function stripQuotes(s) {
|
|
9770
|
+
if (s.length >= 2 && (s[0] === '"' || s[0] === "'") && s[s.length - 1] === s[0]) {
|
|
9771
|
+
return s.slice(1, -1);
|
|
9772
|
+
}
|
|
9773
|
+
return s;
|
|
9774
|
+
}
|
|
9775
|
+
function stripBackticks(s) {
|
|
9776
|
+
if (s.length >= 2 && s[0] === "`" && s[s.length - 1] === "`") {
|
|
9777
|
+
return s.slice(1, -1);
|
|
9778
|
+
}
|
|
9779
|
+
return s;
|
|
9780
|
+
}
|
|
9781
|
+
function hasTemplateSubstitution(node) {
|
|
9782
|
+
for (let i2 = 0; i2 < node.childCount; i2++) {
|
|
9783
|
+
const child = node.child(i2);
|
|
9784
|
+
if (child && child.type === "template_substitution") return true;
|
|
9785
|
+
}
|
|
9786
|
+
return false;
|
|
9787
|
+
}
|
|
9788
|
+
function resolveHandler(node, index) {
|
|
9789
|
+
if (node.type === "arrow_function" || node.type === "function_expression" || node.type === "function") {
|
|
9790
|
+
return {
|
|
9791
|
+
name: null,
|
|
9792
|
+
line: node.startPosition.row + 1,
|
|
9793
|
+
column: node.startPosition.column
|
|
9794
|
+
};
|
|
9795
|
+
}
|
|
9796
|
+
if (node.type === "identifier") {
|
|
9797
|
+
const name2 = getNodeText(node);
|
|
9798
|
+
const decl = index.declarations.get(name2);
|
|
9799
|
+
if (decl) {
|
|
9800
|
+
return { name: name2, line: decl.line, column: decl.column };
|
|
9801
|
+
}
|
|
9802
|
+
return {
|
|
9803
|
+
name: name2,
|
|
9804
|
+
line: node.startPosition.row + 1,
|
|
9805
|
+
column: node.startPosition.column
|
|
9806
|
+
};
|
|
9807
|
+
}
|
|
9808
|
+
if (node.type === "member_expression") {
|
|
9809
|
+
return {
|
|
9810
|
+
name: getNodeText(node),
|
|
9811
|
+
line: node.startPosition.row + 1,
|
|
9812
|
+
column: node.startPosition.column
|
|
9813
|
+
};
|
|
9814
|
+
}
|
|
9815
|
+
return null;
|
|
9816
|
+
}
|
|
9817
|
+
var PY_HTTP_ROUTE_METHODS = /* @__PURE__ */ new Set([
|
|
9818
|
+
// Flask/Blueprint: app.route, blueprint.route, api.route
|
|
9819
|
+
"route",
|
|
9820
|
+
// FastAPI / Starlette / DRF method-specific
|
|
9821
|
+
"get",
|
|
9822
|
+
"post",
|
|
9823
|
+
"put",
|
|
9824
|
+
"patch",
|
|
9825
|
+
"delete",
|
|
9826
|
+
"head",
|
|
9827
|
+
"options"
|
|
9828
|
+
// Flask aliases (Flask 2.x): app.get/post/...
|
|
9829
|
+
]);
|
|
9830
|
+
var PY_MIDDLEWARE_METHODS = /* @__PURE__ */ new Set([
|
|
9831
|
+
"before_request",
|
|
9832
|
+
"after_request",
|
|
9833
|
+
"teardown_request",
|
|
9834
|
+
"before_first_request",
|
|
9835
|
+
"teardown_appcontext",
|
|
9836
|
+
// Starlette / FastAPI
|
|
9837
|
+
"middleware"
|
|
9838
|
+
]);
|
|
9839
|
+
var PY_EVENT_METHODS = /* @__PURE__ */ new Set([
|
|
9840
|
+
"errorhandler",
|
|
9841
|
+
"on_event",
|
|
9842
|
+
"exception_handler"
|
|
9843
|
+
// Celery beat etc — not strictly events but lifecycle
|
|
9844
|
+
]);
|
|
9845
|
+
var PY_STDLIB_DECORATORS = /* @__PURE__ */ new Set([
|
|
9846
|
+
"property",
|
|
9847
|
+
"staticmethod",
|
|
9848
|
+
"classmethod",
|
|
9849
|
+
"abstractmethod",
|
|
9850
|
+
"cached_property",
|
|
9851
|
+
"dataclass",
|
|
9852
|
+
"cache",
|
|
9853
|
+
"lru_cache",
|
|
9854
|
+
"singledispatch",
|
|
9855
|
+
"singledispatchmethod",
|
|
9856
|
+
"contextmanager",
|
|
9857
|
+
"asynccontextmanager",
|
|
9858
|
+
"final",
|
|
9859
|
+
"override",
|
|
9860
|
+
"wraps"
|
|
9861
|
+
]);
|
|
9862
|
+
function summarisePythonImports(imports) {
|
|
9863
|
+
const s = {
|
|
9864
|
+
hasFlask: false,
|
|
9865
|
+
hasFastApi: false,
|
|
9866
|
+
hasCelery: false,
|
|
9867
|
+
hasNumba: false,
|
|
9868
|
+
hasClick: false,
|
|
9869
|
+
hasPytest: false
|
|
9870
|
+
};
|
|
9871
|
+
if (!imports) return s;
|
|
9872
|
+
for (const imp of imports) {
|
|
9873
|
+
const mod = imp.from_package ?? "";
|
|
9874
|
+
if (!mod) continue;
|
|
9875
|
+
if (/^flask(\b|\.)/.test(mod)) s.hasFlask = true;
|
|
9876
|
+
if (/^fastapi(\b|\.)/.test(mod) || /^starlette(\b|\.)/.test(mod)) s.hasFastApi = true;
|
|
9877
|
+
if (/^celery(\b|\.)/.test(mod)) s.hasCelery = true;
|
|
9878
|
+
if (/^numba(\b|\.)/.test(mod)) s.hasNumba = true;
|
|
9879
|
+
if (/^click(\b|\.)/.test(mod)) s.hasClick = true;
|
|
9880
|
+
if (/^pytest(\b|\.)/.test(mod)) s.hasPytest = true;
|
|
9881
|
+
}
|
|
9882
|
+
return s;
|
|
9883
|
+
}
|
|
9884
|
+
function extractPythonRuntimeRegistrations(tree, cache, imports) {
|
|
9885
|
+
const out2 = [];
|
|
9886
|
+
const importSummary = summarisePythonImports(imports);
|
|
9887
|
+
const decoratedDefs = getNodesFromCache(tree.rootNode, "decorated_definition", cache);
|
|
9888
|
+
for (const dd of decoratedDefs) {
|
|
9889
|
+
let fnNode = null;
|
|
9890
|
+
const decorators = [];
|
|
9891
|
+
for (let i2 = 0; i2 < dd.childCount; i2++) {
|
|
9892
|
+
const child = dd.child(i2);
|
|
9893
|
+
if (!child) continue;
|
|
9894
|
+
if (child.type === "decorator") {
|
|
9895
|
+
decorators.push(child);
|
|
9896
|
+
} else if (child.type === "function_definition" || child.type === "async_function_definition") {
|
|
9897
|
+
fnNode = child;
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
if (!fnNode || decorators.length === 0) continue;
|
|
9901
|
+
const handler = pythonHandlerFromFunctionDef(fnNode);
|
|
9902
|
+
if (!handler) continue;
|
|
9903
|
+
for (const dec of decorators) {
|
|
9904
|
+
const parsed = parsePythonDecorator(dec);
|
|
9905
|
+
if (!parsed) continue;
|
|
9906
|
+
const { receiver, method, path, line, column } = parsed;
|
|
9907
|
+
const { kind, framework } = classifyPythonDecorator(
|
|
9908
|
+
receiver,
|
|
9909
|
+
method,
|
|
9910
|
+
importSummary
|
|
9911
|
+
);
|
|
9912
|
+
out2.push({
|
|
9913
|
+
kind,
|
|
9914
|
+
framework,
|
|
9915
|
+
registrar: { method, receiver, line, column },
|
|
9916
|
+
...path !== void 0 ? { path } : {},
|
|
9917
|
+
handler
|
|
9918
|
+
});
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
return out2;
|
|
9922
|
+
}
|
|
9923
|
+
function pythonHandlerFromFunctionDef(fn) {
|
|
9924
|
+
const nameNode = fn.childForFieldName("name");
|
|
9925
|
+
if (!nameNode) return null;
|
|
9926
|
+
return {
|
|
9927
|
+
name: getNodeText(nameNode),
|
|
9928
|
+
line: fn.startPosition.row + 1,
|
|
9929
|
+
column: fn.startPosition.column
|
|
9930
|
+
};
|
|
9931
|
+
}
|
|
9932
|
+
function parsePythonDecorator(dec) {
|
|
9933
|
+
let target = null;
|
|
9934
|
+
for (let i2 = 0; i2 < dec.childCount; i2++) {
|
|
9935
|
+
const child = dec.child(i2);
|
|
9936
|
+
if (!child || child.type === "@") continue;
|
|
9937
|
+
target = child;
|
|
9938
|
+
break;
|
|
9939
|
+
}
|
|
9940
|
+
if (!target) return null;
|
|
9941
|
+
const line = dec.startPosition.row + 1;
|
|
9942
|
+
const column = dec.startPosition.column;
|
|
9943
|
+
if (target.type === "identifier") {
|
|
9944
|
+
return { receiver: "", method: getNodeText(target), line, column };
|
|
9945
|
+
}
|
|
9946
|
+
if (target.type === "attribute") {
|
|
9947
|
+
const { receiver, method } = splitDottedAttribute(target);
|
|
9948
|
+
return { receiver, method, line, column };
|
|
9949
|
+
}
|
|
9950
|
+
if (target.type === "call") {
|
|
9951
|
+
const fnNode = target.childForFieldName("function");
|
|
9952
|
+
if (!fnNode) return null;
|
|
9953
|
+
let receiver = "";
|
|
9954
|
+
let method = "";
|
|
9955
|
+
if (fnNode.type === "identifier") {
|
|
9956
|
+
method = getNodeText(fnNode);
|
|
9957
|
+
} else if (fnNode.type === "attribute") {
|
|
9958
|
+
const split = splitDottedAttribute(fnNode);
|
|
9959
|
+
receiver = split.receiver;
|
|
9960
|
+
method = split.method;
|
|
9961
|
+
} else {
|
|
9962
|
+
method = getNodeText(fnNode);
|
|
9963
|
+
}
|
|
9964
|
+
const path = extractFirstStringArg(target);
|
|
9965
|
+
return { receiver, method, path, line, column };
|
|
9966
|
+
}
|
|
9967
|
+
return null;
|
|
9968
|
+
}
|
|
9969
|
+
function splitDottedAttribute(attr) {
|
|
9970
|
+
const objectNode = attr.childForFieldName("object");
|
|
9971
|
+
const attrNode = attr.childForFieldName("attribute");
|
|
9972
|
+
const method = attrNode ? getNodeText(attrNode) : "";
|
|
9973
|
+
const receiver = objectNode ? getNodeText(objectNode) : "";
|
|
9974
|
+
return { receiver, method };
|
|
9975
|
+
}
|
|
9976
|
+
function extractFirstStringArg(call) {
|
|
9977
|
+
const argsNode = call.childForFieldName("arguments");
|
|
9978
|
+
if (!argsNode) return void 0;
|
|
9979
|
+
for (let i2 = 0; i2 < argsNode.childCount; i2++) {
|
|
9980
|
+
const child = argsNode.child(i2);
|
|
9981
|
+
if (!child) continue;
|
|
9982
|
+
if (child.type === "(" || child.type === ")" || child.type === ",") continue;
|
|
9983
|
+
if (child.type === "string") {
|
|
9984
|
+
return stripPythonStringQuotes(getNodeText(child));
|
|
9985
|
+
}
|
|
9986
|
+
return void 0;
|
|
9987
|
+
}
|
|
9988
|
+
return void 0;
|
|
9989
|
+
}
|
|
9990
|
+
function stripPythonStringQuotes(s) {
|
|
9991
|
+
const m = s.match(/^[bBrRuUfF]{0,2}(['"])(.*)\1$/s);
|
|
9992
|
+
if (m) return m[2];
|
|
9993
|
+
if (s.length >= 2 && (s[0] === '"' || s[0] === "'") && s[s.length - 1] === s[0]) {
|
|
9994
|
+
return s.slice(1, -1);
|
|
9995
|
+
}
|
|
9996
|
+
return s;
|
|
9997
|
+
}
|
|
9998
|
+
function classifyPythonDecorator(receiver, method, imp) {
|
|
9999
|
+
if (!receiver && PY_STDLIB_DECORATORS.has(method)) {
|
|
10000
|
+
return { kind: "decorator", framework: "stdlib" };
|
|
10001
|
+
}
|
|
10002
|
+
if (receiver) {
|
|
10003
|
+
const head = receiver.split(".")[0];
|
|
10004
|
+
if (head === "pytest") {
|
|
10005
|
+
return { kind: "decorator", framework: "pytest" };
|
|
10006
|
+
}
|
|
10007
|
+
if (head === "click") {
|
|
10008
|
+
return { kind: "decorator", framework: "click" };
|
|
10009
|
+
}
|
|
10010
|
+
if (head === "numba" || head === "nb") {
|
|
10011
|
+
return { kind: "decorator", framework: "numba" };
|
|
10012
|
+
}
|
|
10013
|
+
if (head === "celery") {
|
|
10014
|
+
return { kind: "decorator", framework: "celery" };
|
|
10015
|
+
}
|
|
10016
|
+
}
|
|
10017
|
+
if (receiver && PY_HTTP_ROUTE_METHODS.has(method)) {
|
|
10018
|
+
const isRoutey = isPyRouterReceiver(receiver);
|
|
10019
|
+
if (isRoutey) {
|
|
10020
|
+
let framework = "unknown";
|
|
10021
|
+
if (imp.hasFlask) framework = "flask";
|
|
10022
|
+
else if (imp.hasFastApi) framework = "fastapi";
|
|
10023
|
+
else if (method === "route") framework = "flask";
|
|
10024
|
+
else framework = "fastapi";
|
|
10025
|
+
return { kind: "http_route", framework };
|
|
10026
|
+
}
|
|
10027
|
+
}
|
|
10028
|
+
if (receiver && PY_MIDDLEWARE_METHODS.has(method)) {
|
|
10029
|
+
return { kind: "middleware", framework: imp.hasFlask ? "flask" : imp.hasFastApi ? "fastapi" : "unknown" };
|
|
10030
|
+
}
|
|
10031
|
+
if (receiver && PY_EVENT_METHODS.has(method)) {
|
|
10032
|
+
return { kind: "event_listener", framework: imp.hasFlask ? "flask" : imp.hasFastApi ? "fastapi" : "unknown" };
|
|
10033
|
+
}
|
|
10034
|
+
if (method === "task" && imp.hasCelery) {
|
|
10035
|
+
return { kind: "decorator", framework: "celery" };
|
|
10036
|
+
}
|
|
10037
|
+
if (!receiver && (method === "login_required" || method === "require_http_methods" || method === "api_view")) {
|
|
10038
|
+
return { kind: "decorator", framework: "django" };
|
|
10039
|
+
}
|
|
10040
|
+
return { kind: "decorator", framework: "unknown" };
|
|
10041
|
+
}
|
|
10042
|
+
function isPyRouterReceiver(receiver) {
|
|
10043
|
+
const head = receiver.split(".")[0];
|
|
10044
|
+
if (!head) return false;
|
|
10045
|
+
if (["app", "router", "blueprint", "bp", "api", "application"].includes(head)) return true;
|
|
10046
|
+
if (/_(router|bp|blueprint|app|api)$/.test(head)) return true;
|
|
10047
|
+
return false;
|
|
10048
|
+
}
|
|
10049
|
+
|
|
9608
10050
|
// src/analysis/config-loader.ts
|
|
9609
10051
|
var DEFAULT_SOURCES = [
|
|
9610
10052
|
// HTTP Sources (Servlet API)
|
|
@@ -19778,7 +20220,7 @@ function extractEventHandlers(elementNode, eventHandlers) {
|
|
|
19778
20220
|
if (!EVENT_HANDLER_ATTRS.has(attrName)) continue;
|
|
19779
20221
|
const valueNode = findChildByType2(child, "quoted_attribute_value") ?? findChildByType2(child, "attribute_value");
|
|
19780
20222
|
if (!valueNode) continue;
|
|
19781
|
-
const code =
|
|
20223
|
+
const code = stripQuotes2(valueNode.text);
|
|
19782
20224
|
if (code) {
|
|
19783
20225
|
eventHandlers.push({
|
|
19784
20226
|
code,
|
|
@@ -19809,7 +20251,7 @@ function getAttributeValue(tag, name2) {
|
|
|
19809
20251
|
const nameNode = findChildByType2(child, "attribute_name");
|
|
19810
20252
|
if (nameNode?.text.toLowerCase() === name2) {
|
|
19811
20253
|
const valueNode = findChildByType2(child, "quoted_attribute_value") ?? findChildByType2(child, "attribute_value");
|
|
19812
|
-
return valueNode ?
|
|
20254
|
+
return valueNode ? stripQuotes2(valueNode.text) : "";
|
|
19813
20255
|
}
|
|
19814
20256
|
}
|
|
19815
20257
|
return void 0;
|
|
@@ -19821,7 +20263,7 @@ function findChildByType2(node, type) {
|
|
|
19821
20263
|
}
|
|
19822
20264
|
return null;
|
|
19823
20265
|
}
|
|
19824
|
-
function
|
|
20266
|
+
function stripQuotes2(text) {
|
|
19825
20267
|
if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
|
|
19826
20268
|
return text.slice(1, -1);
|
|
19827
20269
|
}
|
|
@@ -25024,7 +25466,7 @@ var SecurityHeadersPass = class {
|
|
|
25024
25466
|
};
|
|
25025
25467
|
function literalOf(arg) {
|
|
25026
25468
|
if (arg.literal !== null && arg.literal !== void 0 && arg.literal !== "") {
|
|
25027
|
-
return
|
|
25469
|
+
return stripQuotes3(arg.literal);
|
|
25028
25470
|
}
|
|
25029
25471
|
const expr = arg.expression.trim();
|
|
25030
25472
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'") || expr.startsWith("`") && expr.endsWith("`")) {
|
|
@@ -25033,7 +25475,7 @@ function literalOf(arg) {
|
|
|
25033
25475
|
}
|
|
25034
25476
|
return null;
|
|
25035
25477
|
}
|
|
25036
|
-
function
|
|
25478
|
+
function stripQuotes3(s) {
|
|
25037
25479
|
if (s.length < 2) return s;
|
|
25038
25480
|
const first = s[0];
|
|
25039
25481
|
const last = s[s.length - 1];
|
|
@@ -26120,7 +26562,9 @@ function getNodeTypesForLanguage(language) {
|
|
|
26120
26562
|
"import_from_statement",
|
|
26121
26563
|
"assignment",
|
|
26122
26564
|
"attribute",
|
|
26123
|
-
"subscript"
|
|
26565
|
+
"subscript",
|
|
26566
|
+
"decorated_definition",
|
|
26567
|
+
"decorator"
|
|
26124
26568
|
]);
|
|
26125
26569
|
case "javascript":
|
|
26126
26570
|
case "typescript":
|
|
@@ -26212,6 +26656,7 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
26212
26656
|
const exports = extractExports(types);
|
|
26213
26657
|
const cfg = buildCFG(tree, language);
|
|
26214
26658
|
const dfg = buildDFG(tree, nodeCache, language);
|
|
26659
|
+
const runtimeRegistrations = extractRuntimeRegistrations(tree, nodeCache, language, imports);
|
|
26215
26660
|
const graph = new CodeGraph({
|
|
26216
26661
|
meta,
|
|
26217
26662
|
types,
|
|
@@ -26306,7 +26751,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
26306
26751
|
unresolved,
|
|
26307
26752
|
enriched,
|
|
26308
26753
|
findings: findings.length > 0 ? findings : void 0,
|
|
26309
|
-
metrics: { file: filePath, metrics: metricValues }
|
|
26754
|
+
metrics: { file: filePath, metrics: metricValues },
|
|
26755
|
+
runtime_registrations: runtimeRegistrations.length > 0 ? runtimeRegistrations : void 0
|
|
26310
26756
|
};
|
|
26311
26757
|
} finally {
|
|
26312
26758
|
disposeTree(tree);
|
|
@@ -8,4 +8,5 @@ export { extractImports } from './imports.js';
|
|
|
8
8
|
export { extractExports } from './exports.js';
|
|
9
9
|
export { buildCFG } from './cfg.js';
|
|
10
10
|
export { buildDFG } from './dfg.js';
|
|
11
|
+
export { extractRuntimeRegistrations } from './runtime-registrations.js';
|
|
11
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/extractors/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/extractors/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -8,4 +8,5 @@ export { extractImports } from './imports.js';
|
|
|
8
8
|
export { extractExports } from './exports.js';
|
|
9
9
|
export { buildCFG } from './cfg.js';
|
|
10
10
|
export { buildDFG } from './dfg.js';
|
|
11
|
+
export { extractRuntimeRegistrations } from './runtime-registrations.js';
|
|
11
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/extractors/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/extractors/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-registration extractor (issue #15 — Phases 1 + 2).
|
|
3
|
+
*
|
|
4
|
+
* Recognises framework registration patterns where a handler is wired into a
|
|
5
|
+
* dispatch table at module-load time. Static call extraction sees the
|
|
6
|
+
* registration call/decorator but not the edge from registrar → handler.
|
|
7
|
+
*
|
|
8
|
+
* Downstream consumers (e.g. dead-code reachability) read
|
|
9
|
+
* `ir.runtime_registrations` and add each resolved handler as a virtual entry
|
|
10
|
+
* root, eliminating "unreachable" false positives for framework handlers.
|
|
11
|
+
*
|
|
12
|
+
* Phase 1 — JS/TS Express-family (shipped 3.32.0):
|
|
13
|
+
* - HTTP routes: `app.METHOD(path?, ...handlers)` for METHOD ∈ HTTP_VERBS
|
|
14
|
+
* - Middleware: `app.use(...handlers)`
|
|
15
|
+
* - Event listeners: `emitter.on('event', handler)` on express-shaped receivers
|
|
16
|
+
*
|
|
17
|
+
* Phase 2 — Python decorators (3.33.0):
|
|
18
|
+
* - Every `@decorator` on a function emits a registration with handler =
|
|
19
|
+
* decorated function. Known frameworks are tagged (flask, fastapi,
|
|
20
|
+
* django, click, pytest, celery, numba); built-in (property,
|
|
21
|
+
* staticmethod, etc.) is tagged `stdlib`. Routing-style decorators
|
|
22
|
+
* (`@app.route`, `@app.get`, `@router.post`) are classified as
|
|
23
|
+
* `kind: 'http_route'` so downstream consumers can treat JS routes and
|
|
24
|
+
* Python routes uniformly.
|
|
25
|
+
*
|
|
26
|
+
* Out of scope (Phase 3):
|
|
27
|
+
* - Rust trait dispatch (`impl Trait for Type`, `Box<dyn Trait>`,
|
|
28
|
+
* `inventory::submit!`, `linkme::distributed_slice`).
|
|
29
|
+
* - Subapp mounting (`app.use('/api', subApp)`) handler resolution.
|
|
30
|
+
*/
|
|
31
|
+
import type { Tree } from 'web-tree-sitter';
|
|
32
|
+
import type { RuntimeRegistration, ImportInfo } from '../../types/index.js';
|
|
33
|
+
import { type NodeCache } from '../parser.js';
|
|
34
|
+
import type { SupportedLanguage } from '../parser.js';
|
|
35
|
+
/**
|
|
36
|
+
* Extract runtime-registration patterns from a parsed file.
|
|
37
|
+
*
|
|
38
|
+
* Phase 1 covers JavaScript/TypeScript. Phase 2 adds Python decorators.
|
|
39
|
+
* Returns `[]` for any other language.
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractRuntimeRegistrations(tree: Tree, cache: NodeCache | undefined, language: SupportedLanguage | string, imports?: ImportInfo[]): RuntimeRegistration[];
|
|
42
|
+
//# sourceMappingURL=runtime-registrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-registrations.d.ts","sourceRoot":"","sources":["../../../src/core/extractors/runtime-registrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAQ,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA2BtD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,QAAQ,EAAE,iBAAiB,GAAG,MAAM,EACpC,OAAO,CAAC,EAAE,UAAU,EAAE,GACrB,mBAAmB,EAAE,CAQvB"}
|