Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions graph/python/engine/resolution/generics.dl
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ param_class_object_bound("client", ph, t) :-
type_ref_nesting("client", r, _, "1", child),
type_ref("client", "TYPE_VAR", _, vn, _, child), vn != "",
typevar_bound("client", vn, t).
// …and the same under a union (`cls: type[CmdType] | None = None`): see param_class_object_ref.
param_class_object_bound("client", ph, t) :-
param_class_object_ref("client", ph, s),
type_ref_owner("client", ph, "METHOD_PARAM", r), s != r,
type_ref_nesting("client", s, _, _, child),
type_ref("client", "TYPE_VAR", _, vn, _, child), vn != "",
typevar_bound("client", vn, t).
expr_type_class_object("client", e, t) :-
expr_names_param("client", e, ph),
param_class_object_bound("client", ph, t).
22 changes: 22 additions & 0 deletions graph/python/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,35 @@ expr_type_class_object(p, e, t) :-
annotation_names_a_class("type").
annotation_names_a_class("Type").

// ── param_class_object_ref(Prov, ParamHash, SubscriptRef) ─────────────────────
// The `type[...]` subscript a parameter's annotation IS, or one operand of its union is.
// `cls: type[Command] | None = None` (and `Optional[type[Command]]`) is the default-None
// spelling of the same class-object parameter: the body replaces None with a default class
// and calls `cls(...)`. Read only at the top level, that call stayed unresolved, and so did
// every constructor reached through it.
param_class_object_ref("client", ph, r) :-
type_ref_owner("client", ph, "METHOD_PARAM", r),
type_ref("client", _, "METHOD_PARAM", tn, _, r),
annotation_names_a_class(tn).
param_class_object_ref("client", ph, s) :-
type_ref_owner("client", ph, "METHOD_PARAM", r),
union_operand("client", r, s),
type_ref("client", _, _, tn, _, s),
annotation_names_a_class(tn).

expr_type_class_object("client", e, t) :-
expr_names_param("client", e, ph),
type_ref_owner("client", ph, "METHOD_PARAM", r),
type_ref("client", _, "METHOD_PARAM", tn, _, r),
annotation_names_a_class(tn),
type_ref_nesting("client", r, _, "1", child),
type_ref_resolved("client", t, child).
expr_type_class_object("client", e, t) :-
expr_names_param("client", e, ph),
param_class_object_ref("client", ph, s),
type_ref_owner("client", ph, "METHOD_PARAM", r), s != r,
type_ref_nesting("client", s, _, _, child),
type_ref_resolved("client", t, child).

// A name written MORE THAN ONCE takes the union, for the same reason the instance side
// does: a sound set beats a blank, and the tier follows from the target count.
Expand Down
1 change: 1 addition & 0 deletions graph/python/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
.decl binding_element_lib_type(c0:symbol,c1:symbol)
.decl param_declared_type_by_parser(c0:symbol,c1:symbol,c2:symbol)
.decl union_operand(c0:symbol,c1:symbol,c2:symbol)
.decl param_class_object_ref(c0:symbol,c1:symbol,c2:symbol)
.decl type_ref_element(c0:symbol,c1:symbol,c2:symbol)
.decl annotation_owner_module(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl annotation_container_kind(c0:symbol)
Expand Down
28 changes: 25 additions & 3 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,12 +671,17 @@ def _string_literals(q):
# 3. a string that names a MEMBER OF A TYPE THE SAME DECORATION NAMES: `@SelectProvider(type = StockSql.class,
# method = "byShelf")` points at StockSql.byShelf; it is a reference to that method, not a key for this one.
# Only decided with the graph (`names_member(type, name)`); without it the string is kept.
_STRING = re.compile(r'"([^"]{1,120})"|\'([^\']{1,120})\'')
# 4. a string that names a PARAMETER OF THE DECLARATION IT DECORATES: `@option("--params", "-p", "params")` on
# `def main(url, params)` binds the value a caller passes after `--params` to `params`. The flags are what a caller
# writes to reach the declaration; the parameter name is written by every function that builds a dict with a
# `params` key, and joined as a key it made each of them a caller of the command.
# 5. a string inside ANOTHER call in the decoration: `type=File("wb")` configures a value, it names nothing registered.
_STRING =re.compile(r'"([^"]{1,120})"|\'([^\']{1,120})\'')
_KEYWORD_BEFORE = re.compile(r'(\w+)\s*[=:]\s*[\[{(]?\s*(?:(?:"[^"]*"|\'[^\']*\')\s*,\s*)*$')
_TYPE_ARG = re.compile(r'(?<![\w."\'$])([A-Z][\w$]*)(?:\s*\.\s*class)?(?=\s*[,)\]}])')


def decoration_key_strings(text, name=None, names_member=None):
def decoration_key_strings(text, name=None, names_member=None, params=None):
"""the strings a decoration's text registers its declaration under, sorted: every quoted string in it but prose,
and but the three shapes above (a non-registering decoration `name`, a configuring keyword, a member reference).
A STRING WITH A SPACE IN IT IS PROSE, NOT A KEY: `@widgets.doc("Endpoint to list the widgets")`, `@Operation(summary = "List
Expand All @@ -693,10 +698,26 @@ def decoration_key_strings(text, name=None, names_member=None):
kw = _KEYWORD_BEFORE.search(t[:m.start()].replace('"""', '"'))
if kw and not _KEY_KEYWORD.match(kw.group(1)): continue
if any(names_member(ty, key) for ty in types): continue
if _call_depth(t, m.start()) > 1: continue
if params and key in params: continue
out.add(key)
return sorted(out)


def _call_depth(t, i):
"""how many parentheses are open at offset i of a decoration's text, strings blanked: 1 is the decoration's own
argument list"""
return _STRING.sub(lambda m: '"' + ' ' * (len(m.group(0)) - 2) + '"', t[:i]).count('(') - \
_STRING.sub(lambda m: '"' + ' ' * (len(m.group(0)) - 2) + '"', t[:i]).count(')')


def _params_of(signature):
"""the parameter names a `name(a, b=1, *c)` signature declares"""
m = re.search(r'\((.*)\)', signature or '')
if not m: return set()
return {re.sub(r'[:=].*$', '', p).strip().lstrip('*') for p in m.group(1).split(',')} - {''}


def member_names(q):
"""names_member for decoration_key_strings, read from the graph: does a type of this simple name declare a member
of that name"""
Expand All @@ -720,13 +741,14 @@ def decoration_keys(q, site_file=None):
# production code; nothing is lost by declining to read a test's own decoration as a registration.
tests = {r[0] for r in q("SELECT id FROM symbols WHERE is_test = 1")} if _has(q, 'symbols') else set()
members = member_names(q)
sigs = dict(q("SELECT id, signature FROM symbols WHERE signature IS NOT NULL")) if _has(q, 'symbols') else {}
out = []
for owner, name, text, f, l in q("""SELECT owner_id, name, text, file, line FROM decorations
WHERE text IS NOT NULL AND text <> '' AND owner_id IS NOT NULL"""):
if owner in tests:
continue
short = (name or '').split('.')[-1]
for key in decoration_key_strings(text, name, members):
for key in decoration_key_strings(text, name, members, _params_of(sigs.get(owner))):
kind = 'route' if key.startswith('/') else 'key'
why = (f'registered as a route "{key}" by @{short} — the router calls it, no call site does' if kind == 'route'
else f'registered under "{key}" by @{short} — whoever writes that string reaches it, and no call site does')
Expand Down
14 changes: 13 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,7 @@ class Impact:
W('cs_fixture_type', sorted(x for x in fixt if not x[0].startswith('collection:')))

# ── facts: the graph, exported once (reused while graph.sqlite is unchanged) ────────────────────────────────
IMPACT_VERSION = '69' # 69: a library-callback site is a library receiver only where a hand-back from it is summarised (ax_edges.library_receiver_sites); 68: both 67s below, from two branches; 67: spawns_fact also links the module a `python -m pkg.mod` spawn runs; 67: libcb, a library summarised to call a client member back (Python library-callbacks.dl), walked as fw_edge "library callback"; 66: imports_fact names a module from its package root (a src layout) and has each test file import the conftest.py files above it; 65: guard_only also reads a conjunctive guard (`cond and __name__ == "__main__"`); 64: guard_only, a module body's call made only under `if __name__ == "__main__":`, which the import walk does not take; 63: imports_fact resolves an import written with its extension and a workspace package's specifier, and test_method takes no JavaScript / TypeScript helper by its name; 62: state_gate, state_gate_alloc, state_call_alloc, state_call_open, state_world, the callbacks one instance was given and the allocation each caller's receiver may be (JavaScript instance-state.dl); 61: filter_run, a servlet filter a test's context holds (added to HttpSecurity in a configuration the test loads, or a component) and the test methods that send a request through it; 57: a TypeScript object literal key is a ref of entity kind OBJECT_PROPERTY_KEY, kept past a bound access on its line; 56: reg_key_fact carries a handler table's entries (kind table), literal a table key written as a dotted string or through a constant, and test_code; 55: cs_data_source, cs_data_type, cs_fixture_type, the C# test links a runner makes from a data attribute or a class/collection fixture (#1498, #1499); 53: implicit_new, the type a C# `new T()` constructs where T writes no constructor (#1473); 52: test_method holds a method under a composed or derived test marker declared in the repository (a Java annotation meta-annotated @Test, a C# attribute derived from FactAttribute: #1418, #1497; 51 was the C# test-links branch's number, landed as 55); 48: sigtype, a parameter / return position type_use resolves to a type, read before the textuse grep (#1422), and persist_field, the properties a persistence query reads (#1461); 47: test_method / fixture from graph_sql's one classification (a tear-down such as @AfterEach or [TestCleanup] is a fixture, [TestInitialize] is no test, an @Override is no named test: #1417 #1419 #1502), and reg_key_fact drops a string a decoration does not register under (#1413); 46: accessor carries the wither and an isX boolean's setX / withX, and a generated builder or fluent setter the engine resolved is a writer (#1404, #1409); 45: runs_before, a C# set-up an NUnit [SetUpFixture] or an MSTest assembly initializer runs for tests outside its type (#1501), stub rows for a member a Moq Protected() setup names by string (#1540), cs_config_literal for a Section:Key configuration key (#1443), and lex_parent puts a lambda under the declaration on its own line (#1556); 44: a C# MEMBER_ACCESS ref is qualified, so its qualifier decides (#1445); 41: spawns_fact, a test that runs a script by its path (ax_spawn.py); 40: test_method holds a script test's module (a test-tree file run as a program, no framework: graph_sql.script_tests); 39: a chained route link's `calls` row and `registration` label sit on the link's own line, with its own verb and path; 38: reg_key_fact drops a decoration string with a space in it (a description, not a key); 37: via_base / via_site, a caller that reaches a declaration through a base it is override-equivalent to (#1542), and injected_bean, the bean an injection point was wired to (#1384); 36: handoff_at, route_arg, callable_const, init_wrapper, init_alias, returns_fn — a const holding a wrapped handler registered at a route; 35: 0.1.5's 33 (#1598, the defines edges the path export links) joined 0.1.6's 33, two different fact sets under one number; 33 (0.1.6): calls carries the tier "stub" for a call inside a mock's stub or verification, reg_verb / lit_verb join a route by its HTTP method, and a handler's route joins its type's prefix; 32: cert_tier's why is worded per tier (an event_dispatch row says it sends the request or event), and the route facts #1633 changed (#1510), which merged without a bump; 31: event_dispatch edges (a published event reaches its listeners, #1391) and the pytest fixture_injection reading (#1631) change impact's facts; 30: registers, a bean another class's annotation registers (#1396, #1414); 29: the edges it links from the path export changed (#1402), and a cache written before it must not survive; 28: reexport_from, the file an `export *` line re-exports; 27: framework, the engine's framework_edge (#1509); 24: the test* naming convention requires a test class as owner (#1181); 23: owner/member disambiguated by file, two classes of one name no longer merging (#1188); 22: lex_parent, the innermost declaration enclosing each one by span (#1183); 21: cert_tier, the tier -> certainty table the call rules join on (#1131); 20: faccess, the engine's resolved field accesses (#1071); 3: decl_file facts (the import-time test-file rule); 14: the registration-key
IMPACT_VERSION = '70' # 70: fn_returns / fn_returns_param / decorated_call (a by-name decorator hop lands on the wrapper the decorator returns), a decoration string naming a parameter of the declaration is no registration key; 69: a library-callback site is a library receiver only where a hand-back from it is summarised (ax_edges.library_receiver_sites); 68: both 67s below, from two branches; 67: spawns_fact also links the module a `python -m pkg.mod` spawn runs; 67: libcb, a library summarised to call a client member back (Python library-callbacks.dl), walked as fw_edge "library callback"; 66: imports_fact names a module from its package root (a src layout) and has each test file import the conftest.py files above it; 65: guard_only also reads a conjunctive guard (`cond and __name__ == "__main__"`); 64: guard_only, a module body's call made only under `if __name__ == "__main__":`, which the import walk does not take; 63: imports_fact resolves an import written with its extension and a workspace package's specifier, and test_method takes no JavaScript / TypeScript helper by its name; 62: state_gate, state_gate_alloc, state_call_alloc, state_call_open, state_world, the callbacks one instance was given and the allocation each caller's receiver may be (JavaScript instance-state.dl); 61: filter_run, a servlet filter a test's context holds (added to HttpSecurity in a configuration the test loads, or a component) and the test methods that send a request through it; 57: a TypeScript object literal key is a ref of entity kind OBJECT_PROPERTY_KEY, kept past a bound access on its line; 56: reg_key_fact carries a handler table's entries (kind table), literal a table key written as a dotted string or through a constant, and test_code; 55: cs_data_source, cs_data_type, cs_fixture_type, the C# test links a runner makes from a data attribute or a class/collection fixture (#1498, #1499); 53: implicit_new, the type a C# `new T()` constructs where T writes no constructor (#1473); 52: test_method holds a method under a composed or derived test marker declared in the repository (a Java annotation meta-annotated @Test, a C# attribute derived from FactAttribute: #1418, #1497; 51 was the C# test-links branch's number, landed as 55); 48: sigtype, a parameter / return position type_use resolves to a type, read before the textuse grep (#1422), and persist_field, the properties a persistence query reads (#1461); 47: test_method / fixture from graph_sql's one classification (a tear-down such as @AfterEach or [TestCleanup] is a fixture, [TestInitialize] is no test, an @Override is no named test: #1417 #1419 #1502), and reg_key_fact drops a string a decoration does not register under (#1413); 46: accessor carries the wither and an isX boolean's setX / withX, and a generated builder or fluent setter the engine resolved is a writer (#1404, #1409); 45: runs_before, a C# set-up an NUnit [SetUpFixture] or an MSTest assembly initializer runs for tests outside its type (#1501), stub rows for a member a Moq Protected() setup names by string (#1540), cs_config_literal for a Section:Key configuration key (#1443), and lex_parent puts a lambda under the declaration on its own line (#1556); 44: a C# MEMBER_ACCESS ref is qualified, so its qualifier decides (#1445); 41: spawns_fact, a test that runs a script by its path (ax_spawn.py); 40: test_method holds a script test's module (a test-tree file run as a program, no framework: graph_sql.script_tests); 39: a chained route link's `calls` row and `registration` label sit on the link's own line, with its own verb and path; 38: reg_key_fact drops a decoration string with a space in it (a description, not a key); 37: via_base / via_site, a caller that reaches a declaration through a base it is override-equivalent to (#1542), and injected_bean, the bean an injection point was wired to (#1384); 36: handoff_at, route_arg, callable_const, init_wrapper, init_alias, returns_fn — a const holding a wrapped handler registered at a route; 35: 0.1.5's 33 (#1598, the defines edges the path export links) joined 0.1.6's 33, two different fact sets under one number; 33 (0.1.6): calls carries the tier "stub" for a call inside a mock's stub or verification, reg_verb / lit_verb join a route by its HTTP method, and a handler's route joins its type's prefix; 32: cert_tier's why is worded per tier (an event_dispatch row says it sends the request or event), and the route facts #1633 changed (#1510), which merged without a bump; 31: event_dispatch edges (a published event reaches its listeners, #1391) and the pytest fixture_injection reading (#1631) change impact's facts; 30: registers, a bean another class's annotation registers (#1396, #1414); 29: the edges it links from the path export changed (#1402), and a cache written before it must not survive; 28: reexport_from, the file an `export *` line re-exports; 27: framework, the engine's framework_edge (#1509); 24: the test* naming convention requires a test class as owner (#1181); 23: owner/member disambiguated by file, two classes of one name no longer merging (#1188); 22: lex_parent, the innermost declaration enclosing each one by span (#1183); 21: cert_tier, the tier -> certainty table the call rules join on (#1131); 20: faccess, the engine's resolved field accesses (#1071); 3: decl_file facts (the import-time test-file rule); 14: the registration-key
# layer; 15: the registration facts (two 14s landed independently, which is exactly the collision this
# guards); 16: regsite folded into ax_registration's reg_key_fact; 20: implements_pair (#1011); 17/18: the tagged-template test registrar
# (it.each`…`) and its table span
Expand Down Expand Up @@ -1745,6 +1745,18 @@ class Impact:
a, b = bym.get(r[0]), bym.get(r[1])
if a and b and a != b: deconame.append((a, b))
W('decorated_name', sorted(set(deconame)))
# …and where the engine could NOT resolve the decoration, what the decorator the name denotes RETURNS, so the
# by-name decorator hop (dl/impact.dl `deco_wrapper`) lands on the wrapper the callers run, not on the
# decorator's own body: `fn_returns(m, w)` m returns the function w, `fn_returns_param(m)` m returns an argument
# it was handed (the registering `return f`), `decorated_call(s, d)` the decoration is written as a call
# (`@route("/x")`: the decorator applied is what `route` returned, not `route`).
bym = {sy['method_id']: i for i, sy in g.sym.items() if sy.get('method_id')}
W('fn_returns', sorted({(bym[a], bym[b]) for a, b in (g.q("SELECT c1, c2 FROM ext_method_returns_method") if g.has('ext_method_returns_method') else [])
if a in bym and b in bym and a != b}))
W('fn_returns_param', sorted({(bym[a],) for (a,) in (g.q("SELECT c1 FROM ext_method_returns_param") if g.has('ext_method_returns_param') else [])
if a in bym}))
W('decorated_call', sorted({(r[0], r[1].split('.')[-1]) for r in (g.q("SELECT owner_id, name, text FROM decorations") if g.has('decorations') else [])
if r[1] and re.match(r'\s*@?\s*[\w.]+\s*\(', r[2] or '')}))
# ── the two spellings of one route ──────────────────────────────────────────────────────────────────────
# A test asks for `/orders/o-1/price`; the handler is registered as `/orders/{order_id}/price`. Neither
# string contains the other, and no call site joins them — the router does, at run time, by matching the
Expand Down
Loading
Loading