Skip to content

Commit ded4acb

Browse files
Merge pull request #1774 from AxiomCodeAI/loop/ja-is1758
javascript: a request handing over a call, an await or a const is not a route registration
2 parents 4a1848f + e5fe241 commit ded4acb

7 files changed

Lines changed: 186 additions & 28 deletions

File tree

‎plugins/axiomcode/skills/axiomcode/scripts/ax_registration.py‎

Lines changed: 97 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,10 @@ def _route_links(q, sf, lits, tf):
156156
mine = [(s, l) for s, _t, l in args if s]
157157
own = [(l, i, v) for l, i, v in own if (v, l) in mine][:1] or [(l, -1, s) for s, l in mine[:1]]
158158
if handlers is None:
159-
handlers = _handler_names(q)
160-
if short.lower() in HTTP_VERB and not any(_hands_over(t, handlers, f, read(f)) for s, t, _l in args if not s):
159+
handlers = _Handlers(q, read, sf)
160+
hands = _any3(_hands_over(t, handlers, f, read(f)) for s, t, _l in args if not s) \
161+
if short.lower() in HTTP_VERB else True
162+
if hands is False or (hands is None and _used_as_promise(read(f), a, _ac, b, col)):
161163
prev_end, prev_path = b, None
162164
continue # a request (a client, a Map, Headers): nothing is registered here
163165
if own:
@@ -279,50 +281,118 @@ def _own_args(L, a, ac, b, bc):
279281
'enum_member', 'property', 'local'}
280282

281283

282-
def _handler_names(q):
283-
"""name -> {kind: {file}} of every declaration, for telling a handler handed over by name from the request's data"""
284-
names = {}
285-
if _has(q, 'symbols'):
286-
for n, k, f in q("SELECT name, kind, file FROM symbols WHERE name IS NOT NULL"):
287-
names.setdefault(n, {}).setdefault(k or '', set()).add(f)
288-
return names
289-
290-
291-
def _names_handler(t, names, f, L):
284+
class _Handlers:
285+
"""What a route call's argument is checked against: `names` (name -> {kind: {file}} of every declaration), the
286+
names of callables the engine says return a function, and — read once per const, from its initializer — whether a
287+
const holds a function (`const h = catchAsync(…)`) or data (`const opts = { headers: {} }`)."""
288+
289+
def __init__(self, q, read, sf):
290+
self.q, self.read, self.sf, self.names, self.consts, self.held = q, read, sf, {}, {}, {}
291+
if _has(q, 'symbols'):
292+
for n, k, f, l in q("SELECT name, kind, file, line FROM symbols WHERE name IS NOT NULL"):
293+
self.names.setdefault(n, {}).setdefault(k or '', set()).add(f)
294+
if k == 'const' and l: self.consts.setdefault(n, set()).add((f, l))
295+
self.returns_fn = set()
296+
if _has(q, 'symbols'):
297+
for w, _m in returned_functions(q):
298+
self.returns_fn.update(n for (n,) in q("SELECT name FROM symbols WHERE method_id = ? AND name IS NOT NULL", w))
299+
300+
def holds_fn(self, n, files=None):
301+
"""a const `n` (declared in one of `files`, or anywhere) holds a function; kept when no declaration of it can be
302+
read, as before its initializer was looked at"""
303+
decls = sorted((f, l) for f, l in self.consts.get(n, ()) if files is None or f in files)
304+
readable = [(f, l, n) for f, l in decls if self.read(f)]
305+
if not readable:
306+
return True
307+
key = tuple(readable)
308+
if key not in self.held:
309+
# a module a const requires may export the handler itself: `const h = require('./h'); app.get('/x', h)`
310+
required = any(re.search(rf'(?<![\w$.]){re.escape(n)}\s*=\s*require\s*\(', (self.read(f) or [''])[l - 1])
311+
for f, l, _n in readable if 0 < l <= len(self.read(f)))
312+
fns, _w, _a = const_values(self.q, self.read, readable, self.sf)
313+
self.held[key] = required or bool(fns)
314+
return self.held[key]
315+
316+
317+
def _names_handler(t, h, f, L):
292318
"""a written name `h` or `a.b.h` is a handler: `h` a function or method; a member of a module this file imports
293-
(`users.signup`, `exports.signup = …` declares nothing); `this.h` a property of the class; or a const (a wrapped
294-
handler, `const h = catchAsync(…)`) this file declares or imports. The same name as a parameter, a local, a field
295-
of a type or some other module's `data` (`api.post('/discussions', data)`) is the request's data."""
319+
(`users.signup`, `exports.signup = …` declares nothing); `this.h` a property of the class; or a const holding a
320+
function (a wrapped handler, `const h = catchAsync(…)`) this file declares or imports. The same name as a parameter,
321+
a local, a field of a type, some other module's `data` (`api.post('/discussions', data)`) or a const holding data
322+
(`const opts = { headers: {} }`, `cfg.options`) is the request's data."""
296323
parts = re.split(r'\s*\??\.\s*', t)
297-
kinds = names.get(parts[-1], {})
324+
kinds = h.names.get(parts[-1], {})
298325
if any(k not in _NOT_CALLABLE for k in kinds):
299326
return True
300327
here = parts[0]
328+
# a call site's file is relative to the indexed source root and a declaration's to the repository (`--src src`:
329+
# `requests.js` and `src/requests.js`), so "this file" is the declaration's file ending in the site's
330+
mine = lambda fs: {x for x in fs if f and (x == f or x.endswith('/' + f))}
301331
if len(parts) > 1 and here == 'this':
302-
return any(f in fs for k, fs in kinds.items() if k in ('const', 'field', 'property'))
332+
return any(mine(fs) for k, fs in kinds.items() if k in ('const', 'field', 'property'))
303333
n = re.escape(here)
304334
imported = bool(re.search(r'\bimport\b[^;]*?\b%s\b[^;]*?\bfrom\b|\b%s\b[^=;\n]*=\s*require\s*\(' % (n, n),
305335
'\n'.join(L or ())))
306336
if len(parts) > 1:
307-
return imported
308-
return f in kinds.get('const', ()) or (imported and 'const' in kinds)
337+
return imported and ('const' not in kinds or h.holds_fn(parts[-1]))
338+
if mine(kinds.get('const', ())):
339+
return h.holds_fn(parts[-1], mine(kinds['const']))
340+
return imported and 'const' in kinds and h.holds_fn(parts[-1])
341+
342+
343+
def _any3(vs):
344+
"""True if one is, else None if one is unknown, else False"""
345+
vs = list(vs)
346+
return True if True in vs else (None if None in vs else False)
309347

310348

311-
def _hands_over(t, names, f=None, L=None):
312-
"""the argument text can be something a router calls: a function, a name declared as a handler, a call (a wrapper or
313-
a middleware factory), an array of those. A string, a number, an object, `new X()` or a name that is not one (a
314-
parameter, a local: the request's data) is not."""
349+
def _hands_over(t, h, f=None, L=None):
350+
"""True when the argument text can be something a router calls: a function, a name declared as a handler, a call
351+
that makes one (a wrapper handed a function or handler, a callable the engine says returns a function), an array of
352+
those. False for the request's data: a string, a number, an object, `new X()`, an `await`, a name that is not a
353+
handler (a parameter, a local, a const holding data), or a call of this repository's own callable handed only data
354+
(`buildConfig()`: the engine read its body and saw no function returned). None for a library call handed only data:
355+
`JSON.stringify(x)` and `swaggerUi.setup(specs)` read alike, and the call's own use tells them apart."""
315356
t = t.strip()
316-
if not t or t[0] in '\'"`{' or t[0].isdigit() or re.match(r'(new|true|false|null|undefined)\b', t):
357+
if not t or t[0] in '\'"`{' or t[0].isdigit() or re.match(r'(new|await|true|false|null|undefined)\b', t):
317358
return False
318359
if t.startswith('...') or _fn_literal(t):
319360
return True
320361
if t.startswith('[') and t.endswith(']'):
321362
b = _blank(t)
322-
return any(_hands_over(t[lo:hi], names, f, L) for lo, hi in _split_args(b, 1, len(b) - 1))
363+
return _any3(_hands_over(t[lo:hi], h, f, L) for lo, hi in _split_args(b, 1, len(b) - 1))
323364
if _CHAIN.fullmatch(t):
324-
return _names_handler(t, names, f, L)
325-
return True # a call, a conditional, an `await`: not read further, and kept
365+
return _names_handler(t, h, f, L)
366+
m = re.match(rf'({_IDENT}(?:\s*\??\.\s*{_IDENT})*)\s*\(', t)
367+
if m:
368+
# a call: `wrap(async (req, res) => …)`, `asyncHandler(listItems)`, curried `wrap(opts)(fn)` or a factory the
369+
# engine says returns a function hands one over
370+
callee = re.split(r'\s*\??\.\s*', m.group(1))[-1]
371+
if callee in h.returns_fn:
372+
return True
373+
b, k = _blank(t), m.end() - 1
374+
while k < len(t) and b[k] == '(':
375+
e = _match(b, k)
376+
if _any3(_hands_over(t[lo:hi], h, f, L) for lo, hi in _split_args(b, k + 1, e - 1)):
377+
return True
378+
k = e
379+
while k < len(t) and t[k] in ' \t': k += 1
380+
# only a bare `f(…)` is this repository's own by its name: `swaggerUi.setup(specs)` is the library's `setup`,
381+
# whatever else of that name the repository declares
382+
own = '.' not in m.group(1) and any(k_ not in _NOT_CALLABLE for k_ in h.names.get(callee, {}))
383+
return False if own else None
384+
return True # a conditional, an expression not read further: kept
385+
386+
387+
def _used_as_promise(L, a, ac, b, bc):
388+
"""the call's result is awaited or chained with `.then` / `.catch` / `.finally`: a request's promise — a router's
389+
registration returns the router, never awaited or thenned"""
390+
if not L or not ac or not bc or b > len(L) or a < 1:
391+
return False
392+
if re.search(r'\bawait\s*$', L[a - 1][:ac - 1]):
393+
return True
394+
rest = '\n'.join([L[b - 1][bc - 1:]] + L[b:b + 2])
395+
return bool(re.match(r'\s*\??\.\s*(then|catch|finally)\s*\(', rest))
326396

327397

328398
# A CONST HOLDING A WRAPPED HANDLER — `const h = catchAsync(async (req, res) => …)`, then `router.get('/a', h)` — is a

‎tests/cases/javascript/http-client-is-not-a-route/case.json‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,43 @@
3434
{"why": "`api.patch(`/users/profile`, data)` hands over a parameter that shares its name with another module's const: data, not a handler",
3535
"run": ["impact", "readProfile", "--kind", "method"],
3636
"want": ["updateProfile", "src/api.js:5"],
37-
"avoid": ["route"]}
37+
"avoid": ["route"]},
38+
{"why": "`axios.get('/api/f1', buildConfig())` hands over a call building data, not a wrapper handed a function",
39+
"run": ["impact", "renderA", "--kind", "method"],
40+
"want": ["f1", "src/requests.js:14"],
41+
"avoid": ["route"]},
42+
{"why": "`axios.get('/api/f2', opts)` hands over a const this file declares, and it holds an object, not a function",
43+
"run": ["impact", "renderB", "--kind", "method"],
44+
"want": ["f2", "src/requests.js:15"],
45+
"avoid": ["route"]},
46+
{"why": "`axios.post('/api/f3', JSON.stringify(x))` hands over a library call handed only data",
47+
"run": ["impact", "renderC", "--kind", "method"],
48+
"want": ["f3", "src/requests.js:16"],
49+
"avoid": ["route"]},
50+
{"why": "`axios.post('/api/f8', await serialize(x))` hands over an awaited value",
51+
"run": ["impact", "renderH", "--kind", "method"],
52+
"want": ["f8", "src/requests.js:17"],
53+
"avoid": ["route"]},
54+
{"why": "`axios.get('/api/f9', cfg.options)` hands over an imported module's const that holds an object",
55+
"run": ["impact", "renderI", "--kind", "method"],
56+
"want": ["f9", "src/requests.js:18"],
57+
"avoid": ["route"]},
58+
{"why": "CONTROL: a wrapper call handed an inline async handler is still a GET route",
59+
"run": ["impact", "showW", "--kind", "method"],
60+
"want": ["registered as a GET route \"/w\"", "src/wrapped.js:15"]},
61+
{"why": "CONTROL: a wrapper call handed a declared handler is still a GET route",
62+
"run": ["impact", "listR", "--kind", "method"],
63+
"want": ["registered as a GET route \"/r\"", "src/wrapped.js:16"]},
64+
{"why": "CONTROL: a factory the engine says returns a function is still a GET route",
65+
"run": ["impact", "makeHandler", "--kind", "method"],
66+
"want": ["registered as a GET route \"/m\"", "src/wrapped.js:17"]},
67+
{"why": "CONTROL: a const holding a wrapped handler is still a GET route",
68+
"run": ["impact", "guarded"],
69+
"want": ["registered as a GET route \"/g\"", "src/wrapped.js:18"]},
70+
{"why": "CONTROL: a const that requires a module exporting the handler is still a GET route",
71+
"run": ["impact", "showItem"],
72+
"want": ["registered as a GET route \"/s\"", "src/wrapped.js:19"]},
73+
{"why": "CONTROL: a library factory handed only data (`swaggerUi.setup(specs, …)`) reads like `JSON.stringify(x)`, but nothing awaits or thens this call: still a GET route",
74+
"run": ["impact", "logDocs", "--kind", "method"],
75+
"want": ["registered as a GET route \"/docs\"", "src/wrapped.js:20"]}
3876
]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const options = { timeout: 1 };
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
// the repository's own `setup`: `swaggerUi.setup(…)` is still the library's
4+
function setup() { return {}; }
5+
6+
module.exports = { setup };
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
3+
module.exports = function showItem(req, res) { res.json({}); };
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import axios from 'axios';
2+
import * as cfg from './cfg';
3+
4+
function buildConfig() { return {}; }
5+
async function serialize(x) { return x; }
6+
const opts = { headers: {} };
7+
8+
export function renderA(r) { return r; }
9+
export function renderB(r) { return r; }
10+
export function renderC(r) { return r; }
11+
export function renderH(r) { return r; }
12+
export function renderI(r) { return r; }
13+
14+
export function f1() { return axios.get('/api/f1', buildConfig()).then(renderA); }
15+
export function f2() { return axios.get('/api/f2', opts).then(renderB); }
16+
export function f3(x) { return axios.post('/api/f3', JSON.stringify(x)).then(renderC); }
17+
export async function f8(x) { return axios.post('/api/f8', await serialize(x)).then(renderH); }
18+
export function f9() { return axios.get('/api/f9', cfg.options).then(renderI); }
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
const express = require('express');
3+
const swaggerUi = require('swagger-ui-express');
4+
const showItem = require('./lib/show');
5+
6+
function wrap(fn) { return (req, res, next) => fn(req, res).catch(next); }
7+
function makeHandler() { return (req, res) => res.end(); }
8+
function showW(req, res) { return res.json([]); }
9+
function listR(req, res) { return res.json([]); }
10+
function logDocs(ui) { return ui; }
11+
const guarded = wrap(async (req, res) => listR(req, res));
12+
const specs = { openapi: '3.0.0' };
13+
14+
const app = express();
15+
app.get('/w', wrap(async (req, res) => showW(req, res)));
16+
app.get('/r', wrap(listR));
17+
app.get('/m', makeHandler());
18+
app.get('/g', guarded);
19+
app.get('/s', showItem);
20+
app.get('/docs', swaggerUi.setup(specs, { onComplete: logDocs }));
21+
22+
module.exports = app;

0 commit comments

Comments
 (0)