Skip to content

Commit 5f219b5

Browse files
routes: keep options-object and base-URL routes; a handler by name must be one
Reading the call's own arguments dropped `fastify.route({ method, url: '/items/:id', handler })` (the path is a property of the options object) and MSW's `http.post(`${env.API_URL}/auth/register`, resolver)` (the path follows a base-URL expression), and it kept `api.post(`/discussions`, data)` because some other module declares a `data`. A handler handed over by name is now a function or method, a member of a module the file imports, a class property through `this`, or a const this file declares or imports. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
1 parent 9da3c0a commit 5f219b5

6 files changed

Lines changed: 89 additions & 13 deletions

File tree

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

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ 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 = {n for (n,) in q("SELECT DISTINCT name FROM symbols WHERE name IS NOT NULL")} if _has(q, 'symbols') else set()
160-
if short.lower() in HTTP_VERB and not any(_hands_over(t, handlers) for s, t, _l in args if not s):
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):
161161
prev_end, prev_path = b, None
162162
continue # a request (a client, a Map, Headers): nothing is registered here
163163
if own:
@@ -257,23 +257,60 @@ def _own_args(L, a, ac, b, bc):
257257
for lo, hi in _split_args(blank, j + 1, close):
258258
t = text[lo:hi].strip()
259259
# the path argument: a string beginning with `/` written in it outside any bracket — `'/items'`, and the
260-
# prefixed `path + '/:id'` a resource helper composes — never one inside a nested call's own arguments
260+
# prefixed `path + '/:id'` a resource helper composes — never one inside a nested call's own arguments. A route
261+
# written as one options object (`fastify.route({ method: 'DELETE', url: '/items/:id', handler })`) holds its
262+
# path as a property value of that object, one bracket in
261263
path, at, d = None, lo + len(text[lo:hi]) - len(text[lo:hi].lstrip()), 0
264+
obj = t.startswith('{')
262265
for k in range(lo, hi):
263266
c = blank[k]
264267
if c in _OPEN: d += 1
265268
elif c in _CLOSE: d -= 1
266-
elif c in '\'"`' and d == 0:
267-
m = re.match(r'([\'"`])(/[^\'"`\n]*)\1', text[k:hi])
268-
if m: path, at = m.group(2), k
269-
break
269+
elif c in '\'"`' and (d == 0 or (obj and d == 1 and blank[:k].rstrip().endswith(':'))):
270+
# a template may put a base URL first: `${env.API_URL}/auth/register` registers `/auth/register`
271+
m = re.match(r'([\'"`])(/[^\'"`\n]*)\1|`(?:\$\{[^}`]*\})+(/[^`\n]*)`', text[k:hi])
272+
if m: path, at = m.group(2) or m.group(3), k
273+
if m or d == 0: break
270274
out.append((path, t, a + text.count('\n', 0, at)))
271275
return out
272276

273277

274-
def _hands_over(t, names):
275-
"""the argument text can be something a router calls: a function, a name the graph declares, a call (a wrapper or a
276-
middleware factory), an array of those. A string, a number, an object, `new X()` or a name nothing declares (a
278+
_NOT_CALLABLE = {'field', 'const', 'var', 'variable', 'param', 'parameter', 'typeparam', 'module', 'type', 'enum',
279+
'enum_member', 'property', 'local'}
280+
281+
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):
292+
"""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."""
296+
parts = re.split(r'\s*\??\.\s*', t)
297+
kinds = names.get(parts[-1], {})
298+
if any(k not in _NOT_CALLABLE for k in kinds):
299+
return True
300+
here = parts[0]
301+
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'))
303+
n = re.escape(here)
304+
imported = bool(re.search(r'\bimport\b[^;]*?\b%s\b[^;]*?\bfrom\b|\b%s\b[^=;\n]*=\s*require\s*\(' % (n, n),
305+
'\n'.join(L or ())))
306+
if len(parts) > 1:
307+
return imported
308+
return f in kinds.get('const', ()) or (imported and 'const' in kinds)
309+
310+
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
277314
parameter, a local: the request's data) is not."""
278315
t = t.strip()
279316
if not t or t[0] in '\'"`{' or t[0].isdigit() or re.match(r'(new|true|false|null|undefined)\b', t):
@@ -282,9 +319,9 @@ def _hands_over(t, names):
282319
return True
283320
if t.startswith('[') and t.endswith(']'):
284321
b = _blank(t)
285-
return any(_hands_over(t[lo:hi], names) for lo, hi in _split_args(b, 1, len(b) - 1))
322+
return any(_hands_over(t[lo:hi], names, f, L) for lo, hi in _split_args(b, 1, len(b) - 1))
286323
if _CHAIN.fullmatch(t):
287-
return re.split(r'\s*\??\.\s*', t)[-1] in names
324+
return _names_handler(t, names, f, L)
288325
return True # a call, a conditional, an `await`: not read further, and kept
289326

290327

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,19 @@
2020
"want": ["registered as a POST route \"/items\"", "src/routes.js:11"]},
2121
{"why": "CONTROL: `delete` is also Map.delete, and a router's delete handing over a declared function is still a route",
2222
"run": ["impact", "removeItem", "--kind", "method"],
23-
"want": ["registered as a DELETE route \"/items/:id\"", "src/routes.js:12"]}
23+
"want": ["registered as a DELETE route \"/items/:id\"", "src/routes.js:12"]},
24+
{"why": "CONTROL: a route written as one options object keeps its path, a property value inside the object",
25+
"run": ["impact", "deleteItem", "--kind", "method"],
26+
"want": ["route \"/items/:id\"", "src/plugin.js:7"]},
27+
{"why": "a `get` whose only argument is an object of data hands over nothing: readItem is not a GET route",
28+
"run": ["impact", "readItem", "--kind", "method"],
29+
"want": ["src/plugin.js:8"],
30+
"avoid": ["route"]},
31+
{"why": "CONTROL: a mock server's handler under a base-URL template keeps its route: the path is the template's own after `${...}`",
32+
"run": ["impact", "registerUser", "--kind", "method"],
33+
"want": ["route \"/auth/register\"", "src/mocks.js:8"]},
34+
{"why": "`api.patch(`/users/profile`, data)` hands over a parameter that shares its name with another module's const: data, not a handler",
35+
"run": ["impact", "readProfile", "--kind", "method"],
36+
"want": ["updateProfile", "src/api.js:5"],
37+
"avoid": ["route"]}
2438
]}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { api } from './client';
2+
3+
export function readProfile(res) { return res; }
4+
5+
export const updateProfile = ({ data }) => readProfile(api.patch(`/users/profile`, data));
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { http } from 'msw';
2+
3+
const env = { API_URL: 'http://localhost' };
4+
5+
function registerUser(info) { return info; }
6+
7+
export const handlers = [
8+
http.post(`${env.API_URL}/auth/register`, async ({ request }) => registerUser(await request.json())),
9+
];
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
3+
function deleteItem(request, reply) { reply.code(204).send(); }
4+
function readItem(key) { return key; }
5+
6+
module.exports = async function items(fastify) {
7+
fastify.route({ method: 'DELETE', url: '/items/:id', handler: deleteItem });
8+
const v = readItem(fastify.cache.get({ key: '/items' }));
9+
return v;
10+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const data = [{ id: 1 }];

0 commit comments

Comments
 (0)