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