@@ -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
0 commit comments