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
3 changes: 3 additions & 0 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ def path(d, code):
def context(d, code):
rows = []
listed = set()
# first: where a name the task writes is called though nothing declares it — the code to write is used there
for u in d.get('called_undeclared', []):
rows.append(('called', site(code, u['at'], f"calls {u['name']}, declared nowhere" + (f" · in {u['in']}" if u.get('in') else '')))); listed.add(u['at'])
for s in d.get('flow', []):
if s.get('repeat_of'): continue
c = s.get('certainty') or 'entry'
Expand Down
33 changes: 33 additions & 0 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-context
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ TEST_PENALTY = 0.05 # demoted by 20x, never dropped: sometimes the test
COMMON_PATH = 0.25 # a word in more than a quarter of the file paths earns no path bonus



def called_undeclared(g, body, cap=8):
"""{name: [(file, line, caller, code)]} for identifiers the task writes that the code CALLS but declares nowhere.
The IDF ranker only knows declared names, so a task about a function the code already calls and nobody has written
yet ("implement numpy_to_votable_dtype") lost that word before ranking. The call sites are where the new function
is used: the one thing a search would find and the graph did not say."""
if not (g.has('call_sites') and g.has('unresolved_sites')): return {}
ids = sorted({w for w in re.findall(r'[A-Za-z_][A-Za-z0-9_]{3,}', body)})
if not ids: return {}
ph = ','.join('?' * len(ids))
declared = {r[0] for r in g.q(f"SELECT DISTINCT name FROM symbols WHERE name IN ({ph}) AND kind NOT IN ('library', 'written')", *ids)}
rows = g.q(f"SELECT cs.callee_name n, cs.file_path f, cs.start_line l, cs.caller_id c FROM call_sites cs "
f"JOIN unresolved_sites u ON u.call_site_id = cs.id WHERE cs.callee_name IN ({ph}) ORDER BY cs.file_path, cs.start_line", *ids)
out = {}; lines = {}
for r in rows:
if r['n'] in declared or len(out.get(r['n'], [])) >= cap: continue
f = g.site_file(r['f']) if hasattr(g, 'site_file') else r['f']
if f not in lines:
try: lines[f] = open(os.path.join(g.repo, f), errors='replace').read().split('\n')
except OSError: lines[f] = []
L = lines[f]; code = L[r['l'] - 1].strip() if r['l'] and r['l'] <= len(L) else ''
out.setdefault(r['n'], []).append((f, r['l'], g.disp(r['c']) if r['c'] in g.sym else '', code[:160]))
return out

def score_symbols(g, terms, use_path=True):
"""{symbol id: (score, matched terms)} — the lexical tier, IDF-weighted.

Expand Down Expand Up @@ -934,6 +958,15 @@ def main(argv):
RESULT['task'], RESULT['repo'] = task, g.repo

body = task_text(task)
# before anything that can stop the answer (no terms, no scope): a name the code calls and nobody declares is an
# answer on its own, and "implement <that name>" is often all the task says
undecl = called_undeclared(g, body)
RESULT['called_undeclared'] = [{'name': n, 'at': f"{f}:{l}", 'in': c, 'code': code} for n, rs in undecl.items() for f, l, c, code in rs]
if undecl:
print("\ncalled but declared nowhere (the task names them; the code already calls them — this is where they are used):")
for n, rs in undecl.items():
print(f" {n} — {len(rs)} call site(s)")
for f, l, c, code in rs: print(f" {f}:{l}: {code}" + (f" [in {c}]" if c else ''))
terms = task_terms(body)
if not terms: die("nothing to search for in that task description")
# what the question names that no graph here holds is said FIRST (#1571), and a scope that exists on disk but holds
Expand Down
11 changes: 11 additions & 0 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
Original file line number Diff line number Diff line change
Expand Up @@ -2295,6 +2295,17 @@ def main(argv):
print(f"axiomcode-scope-declared: {int(scoped_in)}", file=sys.stderr)
targets_files =[t for k, _, pay in targets for t in (pay if isinstance(pay, list) else []) if isinstance(t, str) and t in g.sym]
prof('targets resolved'); res = I.run(targets); prof('souffle done')
# A NAME THAT IS CALLED BUT DECLARED NOWHERE. The path finder's resolver takes such a name to the call sites written
# with it (a node `w:<name>`, its rows in g.SITES), but the rules cannot seed on a node the graph does not hold, so
# the answer said "directly touches it: nothing" and "the change is local" about a name with callers. That is the
# question asked when writing a function the code already calls: each of those sites is a direct row, by name.
for q_, lab in res['_targets'].items():
for k, klab, ids in targets:
if k != 'name match' or klab != lab: continue
for i in ids:
for r in getattr(g, 'SITES', {}).get(i, []):
res['direct'].append([r['caller_id'], 'uses', 'calls it (no declaration of this name)', 'by name',
g.site_file(r['file_path']) or '', str(r['start_line'] or 0), q_])
LAB = res['_targets']; multi = len(LAB) > 1
contract_of = collections.defaultdict(set)
for c, why, q_ in res['contract']: contract_of[(c, why)].add(LAB[q_])
Expand Down
16 changes: 16 additions & 0 deletions tests/cases/python/called-but-undeclared/case.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{"lang": "python", "src": "src",
"checks": [
{"why": "a function the code calls and nobody has written yet: impact lists every call site, where the new function is used",
"run": ["impact", "apply_discount"],
"want": ["[by name] total", "pricing.py:6", "[by name] invoice", "pricing.py:10", "calls it (no declaration of this name)"],
"avoid": ["directly touches it: nothing the graph can see", "the change is local"]},
{"why": "the same answer from a task that names the missing function, even when it is the task's only word the graph lacks",
"run": ["context", "implement apply_discount"], "expect_error": true,
"want": ["called but declared nowhere", "apply_discount — 2 call site(s)", "pricing.py:6: return apply_discount(net) * (1 + vat_rate()) [in total]"]},
{"why": "control: a declared function is never listed as called-but-undeclared",
"run": ["context", "how is vat_rate used in total"],
"avoid": ["called but declared nowhere"]},
{"why": "control: a declared function's impact answers from the graph, not by name",
"run": ["impact", "vat_rate"],
"want": ["[resolved] total"],
"avoid": ["no declaration of this name"]}]}
Empty file.
10 changes: 10 additions & 0 deletions tests/cases/python/called-but-undeclared/src/shop/pricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from shop.rates import vat_rate


def total(items):
net = sum(i.price for i in items)
return apply_discount(net) * (1 + vat_rate())


def invoice(items):
return {"total": total(items), "net": apply_discount(sum(i.price for i in items))}
2 changes: 2 additions & 0 deletions tests/cases/python/called-but-undeclared/src/shop/rates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def vat_rate():
return 0.2
Loading