You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is your feature request related to a problem? Please describe.
A new codeanalyzer-python release (v1.4.1) is published. Update the codeanalyzer-python pin in this repo and adapt the integration to the changes below (see PyCodeanalyzer._run_analyzer).
Describe the solution you'd like
Not stated in the original issue.
Describe alternatives you've considered
Not stated in the original issue.
Additional context
[1.4.1] - 2026-09-05
Added
A framework-independent heuristic tier for entrypoints. A decorator whose written spelling reads as an HTTP hook (route, *.route, *.*.route, or *.get/.post/.put/.patch/.delete/.head/.options/.websocket, one or two segments deep) is recorded with framework: "heuristic", confidence: "heuristic" and rule id heuristic.http-route / heuristic.http-verb, whether or not any framework was detected or any framework rule knows the library. Route and methods come from the arguments the same way as framework rules; a node a framework rule already matched gets no heuristic record. Shipped as a heuristics: block in rules.yml, disableable by id like every other rule. * now keeps its meaning inside a {a,b} alternation.
BodyNode.id and PyCallableParameter.id in analysis.json (TSNeo4jBackend.get_call_graph() omits synthesized anonymous-callback nodes — backend parity gap #176). A body node's id is the global ordinal <callable-id>@<local> the Neo4j projection already merges :PyBodyNode on, present at every level the node exists; a parameter's id is <callable-id>@formal_in:<i> for its position i, the level-4 formal_in vertex that carries it (a forward reference below -a 4). Both projections now name a body node the same way, so consumers stop recomposing the can:// grammar themselves. Additive; schema_version and the graph contract are unchanged.
odoo joins the shipped entrypoint rules: @http.route methods (route from the first positional, one route or a list; methods from methods=, default GET) and http.Controller subclasses.
:PyCanNode marker label on every node keyed by a can://python/ id, with a range index on id. It is the anchor the prefix predicates seek on; scope comes from the prefix, not the label. :PyExternal nodes carry it too, so external→application PY_CALLS edges sit inside the application scope (feat(typescript): add tsc_only toggle and surface synthesized anonymous callables #179).
Changed
The odoo entrypoint rule's default methods are [GET, POST], not [GET]: Odoo serves both on a route unless methods= narrows it, and json-typed routes are POST.
BREAKING (graph contract, version stays 2.0.0 — the v2 line has no released consumer): every destructive Neo4j statement is scoped on the can:// id prefix, and the internal _module property retires from every node, from the catalog and from its six per-label indexes (Reachability API: path witnesses between a source and a sink #173, epic Scope every destructive Neo4j statement on the can:// id prefix; retire _module .github#50). The per-module purge matches the module by id and its subtree by id STARTS WITH <module-id> + '/'; the full-run orphan prune and the snapshot wipe match can://python/<app>/. Two python applications sharing a module path no longer delete each other's nodes; an empty application id is refused instead of matching the whole store. Migration: a consumer that filtered on x._module filters on x.id STARTS WITH 'can://python/<app>/<file>/' instead.
BREAKING::PyAttribute and :PyVariable ids are minted from the owner's can:// id — <class-id>/<name> and <owner-id>/<name>@<line> — instead of from its signature. The old ids (service.Service.name) carried no application segment, so two applications MERGEd onto one node.
A call target nobody homed now lands on an @external ghost under the application prefix (can://python/<app>/@external/<module>/<name>), never on a bare-signature id, so every id-keyed node is a can:// node.
Fixed
PY_EXTENDS is now actually emitted (Uptake codeanalyzer-python 0.3.0 (breaking: CodeQL removed, use analysis_level) #178). The projector handed the written base spelling (Base, views.View) to a lookup keyed by signature (pkg.mod.Base), so every inheritance edge was dropped as dangling and no graph since 2.0.0 carried one. Bases now resolve per module: a class declared in the module or reachable through its import table lands on that class's can:// id; anything else lands on an @external ghost with the same id shape call targets use. Relative imports resolve through resolved_module for entrypoint base matching too.
PyEntrypointReport.unresolved no longer counts what is nameable: Python builtins (object, Exception, str), subscripted generics (typing.Generic[T], dict[K, V]) and any spelling whose head is an imported name or a declared class. On Odoo it listed 24 Exception and 20 object bases as unresolved.
The Bolt writer's content_hash diff compared the module row's can:// id against a file key and never matched, so every module counted as changed on every push. It now reads the module row directly, and keys the database side by module id under the application prefix: keyed by file key it was application-blind, so a second application whose module shared the path and the hash looked "unchanged" and was never written.
Is your feature request related to a problem? Please describe.
A new
codeanalyzer-pythonrelease (v1.4.1) is published. Update thecodeanalyzer-pythonpin in this repo and adapt the integration to the changes below (seePyCodeanalyzer._run_analyzer).Describe the solution you'd like
Not stated in the original issue.
Describe alternatives you've considered
Not stated in the original issue.
Additional context
[1.4.1] - 2026-09-05
Added
route,*.route,*.*.route, or*.get/.post/.put/.patch/.delete/.head/.options/.websocket, one or two segments deep) is recorded withframework: "heuristic",confidence: "heuristic"and rule idheuristic.http-route/heuristic.http-verb, whether or not any framework was detected or any framework rule knows the library. Route and methods come from the arguments the same way as framework rules; a node a framework rule already matched gets no heuristic record. Shipped as aheuristics:block inrules.yml, disableable by id like every other rule.*now keeps its meaning inside a{a,b}alternation.BodyNode.idandPyCallableParameter.idinanalysis.json(TSNeo4jBackend.get_call_graph() omits synthesized anonymous-callback nodes — backend parity gap #176). A body node'sidis the global ordinal<callable-id>@<local>the Neo4j projection already merges:PyBodyNodeon, present at every level the node exists; a parameter'sidis<callable-id>@formal_in:<i>for its positioni, the level-4formal_invertex that carries it (a forward reference below-a 4). Both projections now name a body node the same way, so consumers stop recomposing thecan://grammar themselves. Additive;schema_versionand the graph contract are unchanged.:PyApplicationcarriesentrypoint_frameworks(string[]) andentrypoint_report_json(the wholePyEntrypointReport), always present, so a graph consumer can tell "no entrypoints" from "the pass found nothing". Additive graph-catalog change.odoojoins the shipped entrypoint rules:@http.routemethods (route from the first positional, one route or a list; methods frommethods=, defaultGET) andhttp.Controllersubclasses.from odoo import httpplus@http.routematchesodoo.http.routewith odoo not importable in the analysis environment, which is every--no-venvrun.:PyCanNodemarker label on every node keyed by acan://python/id, with a range index onid. It is the anchor the prefix predicates seek on; scope comes from the prefix, not the label.:PyExternalnodes carry it too, so external→applicationPY_CALLSedges sit inside the application scope (feat(typescript): add tsc_only toggle and surface synthesized anonymous callables #179).Changed
[GET, POST], not[GET]: Odoo serves both on a route unlessmethods=narrows it, and json-typed routes are POST.can://id prefix, and the internal_moduleproperty retires from every node, from the catalog and from its six per-label indexes (Reachability API: path witnesses between a source and a sink #173, epic Scope every destructive Neo4j statement on thecan://id prefix; retire_module.github#50). The per-module purge matches the module by id and its subtree byid STARTS WITH <module-id> + '/'; the full-run orphan prune and the snapshot wipe matchcan://python/<app>/. Two python applications sharing a module path no longer delete each other's nodes; an empty application id is refused instead of matching the whole store. Migration: a consumer that filtered onx._modulefilters onx.id STARTS WITH 'can://python/<app>/<file>/'instead.:PyAttributeand:PyVariableids are minted from the owner'scan://id —<class-id>/<name>and<owner-id>/<name>@<line>— instead of from its signature. The old ids (service.Service.name) carried no application segment, so two applications MERGEd onto one node.@externalghost under the application prefix (can://python/<app>/@external/<module>/<name>), never on a bare-signature id, so every id-keyed node is acan://node.Fixed
PY_EXTENDSis now actually emitted (Uptake codeanalyzer-python 0.3.0 (breaking: CodeQL removed, use analysis_level) #178). The projector handed the written base spelling (Base,views.View) to a lookup keyed by signature (pkg.mod.Base), so every inheritance edge was dropped as dangling and no graph since 2.0.0 carried one. Bases now resolve per module: a class declared in the module or reachable through its import table lands on that class'scan://id; anything else lands on an@externalghost with the same id shape call targets use. Relative imports resolve throughresolved_modulefor entrypoint base matching too.PyEntrypointReport.unresolvedno longer counts what is nameable: Python builtins (object,Exception,str), subscripted generics (typing.Generic[T],dict[K, V]) and any spelling whose head is an imported name or a declared class. On Odoo it listed 24Exceptionand 20objectbases as unresolved.PyEntrypointReport.unresolvedis now written (Cached analysis.json not invalidated when tsc_only (resolver mode) changes — stale call graph returned #177). It had no writer at all, so it read{}on every run; it now counts, per written spelling, the decorators and base classes that neither Jedi nor the import table could name.content_hashdiff compared the module row'scan://id against a file key and never matched, so every module counted as changed on every push. It now reads the module row directly, and keys the database side by module id under the application prefix: keyed by file key it was application-blind, so a second application whose module shared the path and the hash looked "unchanged" and was never written.