Skip to content

Commit 8ddd9b1

Browse files
routes: a request is not a registration — the path and a handler must be the call's own arguments
`axios.get('/api/items').then(renderItems)`, `axios.post('/api/items', item)` and `checkD(request.cookies.get('t')) ... redirect('/login')` were read as route registrations: an HTTP-verb name plus any '/...' literal on the same line was enough, and every callable on that line was labelled "registered as a GET route". A route link now reads its own argument list from the source: the path must be one of its arguments, and a call named for an HTTP method must also hand over something callable (a function, a declared name, a call, an array of those). Unreadable or edited source keeps the line-only reading. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
1 parent 3020632 commit 8ddd9b1

5 files changed

Lines changed: 176 additions & 0 deletions

File tree

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

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,28 @@ def _route_links(q, sf, lits, tf):
138138
chains.setdefault((s[0], s[1], s[2]) if s[2] is not None else s, []).append(s)
139139
out, owned, own_line = {}, set(), {}
140140
verbs = {} # (file, line) -> {HTTP verb: first column}, for registrations' wording
141+
read, handlers = _source_reader(q), None
141142
for links in chains.values():
142143
links.sort(key=lambda s: (s[3], s[4] or 0))
143144
taken, prev_end, prev_path = set(), None, None
144145
for f, a, _ac, b, col, short, sid in links:
145146
lo = a if prev_end is None else prev_end
146147
own = [(l, i, v) for l in range(lo, b + 1) for i, v in enumerate(lits.get((f, l), ()))
147148
if isinstance(v, str) and v.startswith('/') and (l, i) not in taken]
149+
args = _own_args(read(f), a, _ac, b, col) if short.lower() in ROUTE_VERB else None
150+
if args is not None:
151+
# THE CALL'S OWN ARGUMENTS, READ FROM THE SOURCE. A path literal on the same line is not evidence:
152+
# `checkD(request.cookies.get('t')) … redirect('/login')` put redirect's path on the `get`, and
153+
# `axios.get('/api/items').then(renderItems)` is a request that hands nothing to anyone. The path must
154+
# be one of this call's arguments, and a call named for an HTTP method must also hand over something
155+
# that can be called: a function, a declared name, a wrapper call, an array of those
156+
mine = [(s, l) for s, _t, l in args if s]
157+
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]]
158+
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):
161+
prev_end, prev_path = b, None
162+
continue # a request (a client, a Map, Headers): nothing is registered here
148163
if own:
149164
l0, i0, path = own[0]
150165
taken.add((l0, i0))
@@ -171,6 +186,108 @@ def _route_links(q, sf, lits, tf):
171186
return out, verbs, own_line
172187

173188

189+
def _source_reader(q):
190+
"""file -> [line] as indexed, from the repository index_meta names; None where it cannot be read. A graph
191+
whose source is not there (copied elsewhere, or read without its tree) keeps the line-only reading."""
192+
import os
193+
roots = []
194+
if _has(q, 'index_meta'):
195+
roots = [v for k in ('repo', 'source_dir') for (v,) in q("SELECT value FROM index_meta WHERE key = ?", k) if v]
196+
cache = {}
197+
198+
def read(f):
199+
if f not in cache:
200+
cache[f] = None
201+
for p in ([f] if os.path.isabs(f or '') else [os.path.join(r, f) for r in roots if f]):
202+
try:
203+
with open(p, errors='replace') as h: cache[f] = h.read().split('\n'); break
204+
except OSError:
205+
pass
206+
return cache[f]
207+
return read
208+
209+
210+
def _blank(t):
211+
"""t with the insides of string literals and comments blanked (same length, quotes kept)"""
212+
out, i, n = list(t), 0, len(t)
213+
while i < n:
214+
c = t[i]
215+
if c in '\'"`':
216+
j = i + 1
217+
while j < n and t[j] != c and (c == '`' or t[j] != '\n'):
218+
if t[j] == '\\': out[j] = ' '; j += 1
219+
if j < n and t[j] != '\n': out[j] = ' '
220+
j += 1
221+
i = j + 1
222+
elif t.startswith('//', i):
223+
j = t.find('\n', i); j = n if j < 0 else j
224+
for k in range(i, j): out[k] = ' '
225+
i = j
226+
elif t.startswith('/*', i):
227+
j = t.find('*/', i + 2); j = n if j < 0 else j + 2
228+
for k in range(i, j):
229+
if t[k] != '\n': out[k] = ' '
230+
i = j
231+
else:
232+
i += 1
233+
return ''.join(out)
234+
235+
236+
def _own_args(L, a, ac, b, bc):
237+
"""[(path or None, argument text, line)] of the call site's OWN argument list — the last one, ending at its end
238+
column — or None when the source does not hold a call there (not readable, or edited since it was indexed)."""
239+
if not L or not bc or not ac or b > len(L) or a < 1:
240+
return None
241+
chunk = [L[a - 1][ac - 1:]] + L[a:b] if b > a else [L[a - 1][ac - 1:bc - 1]]
242+
if b > a: chunk[-1] = chunk[-1][:bc - 1]
243+
text = '\n'.join(chunk); blank = _blank(text)
244+
close = len(blank) - 1
245+
if close < 0 or blank[close] != ')':
246+
return None
247+
d, j = 0, close
248+
while j >= 0:
249+
if blank[j] in _CLOSE: d += 1
250+
elif blank[j] in _OPEN:
251+
d -= 1
252+
if d == 0: break
253+
j -= 1
254+
if j < 0:
255+
return None
256+
out = []
257+
for lo, hi in _split_args(blank, j + 1, close):
258+
t = text[lo:hi].strip()
259+
# 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
261+
path, at, d = None, lo + len(text[lo:hi]) - len(text[lo:hi].lstrip()), 0
262+
for k in range(lo, hi):
263+
c = blank[k]
264+
if c in _OPEN: d += 1
265+
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
270+
out.append((path, t, a + text.count('\n', 0, at)))
271+
return out
272+
273+
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
277+
parameter, a local: the request's data) is not."""
278+
t = t.strip()
279+
if not t or t[0] in '\'"`{' or t[0].isdigit() or re.match(r'(new|true|false|null|undefined)\b', t):
280+
return False
281+
if t.startswith('...') or _fn_literal(t):
282+
return True
283+
if t.startswith('[') and t.endswith(']'):
284+
b = _blank(t)
285+
return any(_hands_over(t[lo:hi], names) for lo, hi in _split_args(b, 1, len(b) - 1))
286+
if _CHAIN.fullmatch(t):
287+
return re.split(r'\s*\??\.\s*', t)[-1] in names
288+
return True # a call, a conditional, an `await`: not read further, and kept
289+
290+
174291
# A CONST HOLDING A WRAPPED HANDLER — `const h = catchAsync(async (req, res) => …)`, then `router.get('/a', h)` — is a
175292
# field, not a method, so it has no call edge of its own to carry the route wording. What both backends join instead
176293
# (`const_route` in dl/impact.dl, graph_sql.direct_for_field) is two facts read from the source at the call sites the
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{"lang": "javascript", "src": "src",
2+
"checks": [
3+
{"why": "`axios.get('/api/items').then(renderItems)` is a request, not a registration: the path is the call's argument but nothing callable is handed to it, so renderItems (handed to .then) was labelled a GET route",
4+
"run": ["impact", "renderItems", "--kind", "method"],
5+
"want": ["loadItems", "src/client.js:8"],
6+
"avoid": ["route"]},
7+
{"why": "`axios.post('/api/items', item)` hands over data (a parameter no declaration names), not a handler",
8+
"run": ["impact", "onSaved", "--kind", "method"],
9+
"want": ["src/client.js:11"],
10+
"avoid": ["route"]},
11+
{"why": "`request.cookies.get('t')` has no path of its own: the '/login' on the line is redirect's argument, not get's",
12+
"run": ["impact", "readToken", "--kind", "method"],
13+
"want": ["guard", "src/client.js:14"],
14+
"avoid": ["route"]},
15+
{"why": "CONTROL: a router registration handing over a declared function is still a GET route",
16+
"run": ["impact", "listItems", "--kind", "method"],
17+
"want": ["registered as a GET route \"/items\"", "src/routes.js:10"]},
18+
{"why": "CONTROL: an inline arrow handler after middleware is still a POST route",
19+
"run": ["impact", "createItem", "--kind", "method"],
20+
"want": ["registered as a POST route \"/items\"", "src/routes.js:11"]},
21+
{"why": "CONTROL: `delete` is also Map.delete, and a router's delete handing over a declared function is still a route",
22+
"run": ["impact", "removeItem", "--kind", "method"],
23+
"want": ["registered as a DELETE route \"/items/:id\"", "src/routes.js:12"]}
24+
]}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import axios from 'axios';
2+
3+
export function renderItems(res) { return res.data; }
4+
export function onSaved(res) { return res.status; }
5+
export function readToken(value) { return value; }
6+
7+
export function loadItems() {
8+
return axios.get('/api/items').then(renderItems);
9+
}
10+
export function saveItem(item) {
11+
return axios.post('/api/items', item).then((r) => onSaved(r));
12+
}
13+
export function guard(request, Response) {
14+
if (!readToken(request.cookies.get('t'))) return Response.redirect('/login');
15+
return null;
16+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
function requireUser(req, res, next) { next(); }
4+
5+
module.exports = { requireUser };
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict';
2+
const express = require('express');
3+
const { requireUser } = require('./lib/auth');
4+
5+
function listItems(req, res) { res.json([]); }
6+
function createItem(req, res) { res.status(201).end(); }
7+
function removeItem(req, res) { res.status(204).end(); }
8+
9+
const router = express.Router();
10+
router.get('/items', listItems);
11+
router.post('/items', requireUser, (req, res) => createItem(req, res));
12+
router.delete('/items/:id', removeItem);
13+
14+
module.exports = router;

0 commit comments

Comments
 (0)