local-codebase-indexer 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- local_codebase_indexer/__init__.py +3 -0
- local_codebase_indexer/__main__.py +5 -0
- local_codebase_indexer/callgraph.py +279 -0
- local_codebase_indexer/cli.py +122 -0
- local_codebase_indexer/core.py +323 -0
- local_codebase_indexer/cscalls.py +304 -0
- local_codebase_indexer/csdata.py +450 -0
- local_codebase_indexer/dataflow.py +250 -0
- local_codebase_indexer/db.py +194 -0
- local_codebase_indexer/embedder.py +137 -0
- local_codebase_indexer/langparse.py +569 -0
- local_codebase_indexer/mcp_server.py +136 -0
- local_codebase_indexer/query.py +756 -0
- local_codebase_indexer/scanner.py +101 -0
- local_codebase_indexer/sqlaccess.py +301 -0
- local_codebase_indexer/synonyms.py +101 -0
- local_codebase_indexer-0.2.0.dist-info/METADATA +118 -0
- local_codebase_indexer-0.2.0.dist-info/RECORD +21 -0
- local_codebase_indexer-0.2.0.dist-info/WHEEL +5 -0
- local_codebase_indexer-0.2.0.dist-info/entry_points.txt +2 -0
- local_codebase_indexer-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Data access found in C# code: SQL strings, stored procedure calls (Dapper / ADO.NET) and EF table mappings.
|
|
2
|
+
|
|
3
|
+
Strings are evaluated statically: literals, ``+`` concatenation, interpolation, ``string.Format``, constants and
|
|
4
|
+
local variables (including ``sql += ...``). Parts that cannot be evaluated become holes. A SQL string handed to a
|
|
5
|
+
known database API is ``confirmed`` when it is fully static and ``probable`` otherwise; a SQL-looking string that
|
|
6
|
+
is not provably executed is ``probable``; a table name that is itself a hole, or an execution site whose SQL could
|
|
7
|
+
not be determined at all, is ``uncertain``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from .sqlaccess import HOLE, Access, extract_sql_accesses
|
|
16
|
+
|
|
17
|
+
_STRINGISH = {"string_literal", "verbatim_string_literal", "raw_string_literal", "interpolated_string_expression"}
|
|
18
|
+
_SCOPES = {
|
|
19
|
+
"method_declaration", "constructor_declaration", "destructor_declaration", "operator_declaration",
|
|
20
|
+
"indexer_declaration", "property_declaration",
|
|
21
|
+
}
|
|
22
|
+
_SINK_METHOD = re.compile(r"^(Execute\w*|Query\w*|FromSql\w*|SqlQuery\w*|Fill)$")
|
|
23
|
+
_DAPPER_STRICT = re.compile(r"^(Execute|Query)\w*$")
|
|
24
|
+
_COMMAND_TYPE = re.compile(r"(Command|DataAdapter)$")
|
|
25
|
+
_PROC_NAME = re.compile(r"^[A-Za-z_][\w$]*(?:\.[\w$]+){0,2}$")
|
|
26
|
+
_NOT_PROC = {"select", "update", "delete", "insert", "exec", "execute", "merge", "truncate"}
|
|
27
|
+
_STATEMENT_SHAPES = [
|
|
28
|
+
re.compile(r"^\s*select\b[\s\S]+\bfrom\b\s+\S", re.I),
|
|
29
|
+
re.compile(r"^\s*insert\s+(?:into\s+)?\S+[\s\S]*\b(?:values|select|exec(?:ute)?)\b", re.I),
|
|
30
|
+
re.compile(r"^\s*update\s+\S+\s+set\b", re.I),
|
|
31
|
+
re.compile(r"^\s*delete\s+(?:from\s+)?\S+[\s\S]*(?:\bwhere\b|@\w)", re.I),
|
|
32
|
+
re.compile(r"^\s*merge\b[\s\S]+\busing\b", re.I),
|
|
33
|
+
re.compile(r"^\s*truncate\s+table\s+\S+", re.I),
|
|
34
|
+
re.compile(r"^\s*exec(?:ute)?\s+[\w.\[\]]*[_.@][\w.\[\]]*", re.I),
|
|
35
|
+
re.compile(r"^\s*with\s+\w+\s+as\s*\([\s\S]+\bselect\b", re.I),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class _Cand:
|
|
41
|
+
text: str
|
|
42
|
+
complete: bool
|
|
43
|
+
line: int
|
|
44
|
+
sink: bool = False
|
|
45
|
+
proc_ok: bool = False # may be read as a stored procedure name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def looks_like_statement(text: str) -> bool:
|
|
49
|
+
return any(rx.search(text) for rx in _STATEMENT_SHAPES)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _walk(node, type_name):
|
|
53
|
+
stack = [node]
|
|
54
|
+
while stack:
|
|
55
|
+
n = stack.pop()
|
|
56
|
+
if n.type == type_name:
|
|
57
|
+
yield n
|
|
58
|
+
stack.extend(reversed(n.children))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Extractor:
|
|
62
|
+
def __init__(self, src: bytes, symbols: list):
|
|
63
|
+
self.src = src
|
|
64
|
+
self.out: list[Access] = []
|
|
65
|
+
spans = [(s.end - s.start, s) for s in symbols if s.type != "namespace"]
|
|
66
|
+
spans.sort(key=lambda t: (t[0], t[1].start))
|
|
67
|
+
self.spans = [s for _, s in spans]
|
|
68
|
+
self.file_env: dict[str, _Cand] = {}
|
|
69
|
+
self.cands: list[_Cand] = []
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------- helpers
|
|
72
|
+
def text(self, n) -> str:
|
|
73
|
+
return self.src[n.start_byte:n.end_byte].decode("utf-8", errors="replace")
|
|
74
|
+
|
|
75
|
+
def locate(self, line: int):
|
|
76
|
+
for s in self.spans:
|
|
77
|
+
if s.start <= line <= s.end:
|
|
78
|
+
return s
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def line(n) -> int:
|
|
83
|
+
return n.start_point[0] + 1
|
|
84
|
+
|
|
85
|
+
def collapsed(self, n) -> str:
|
|
86
|
+
return " ".join(self.text(n).split())[:160]
|
|
87
|
+
|
|
88
|
+
def method_name(self, func) -> str | None:
|
|
89
|
+
if func is None:
|
|
90
|
+
return None
|
|
91
|
+
t = func.type
|
|
92
|
+
if t == "identifier":
|
|
93
|
+
return self.text(func)
|
|
94
|
+
if t == "generic_name":
|
|
95
|
+
for c in func.children:
|
|
96
|
+
if c.type == "identifier":
|
|
97
|
+
return self.text(c)
|
|
98
|
+
if t == "member_access_expression":
|
|
99
|
+
return self.method_name(func.child_by_field_name("name"))
|
|
100
|
+
if t == "conditional_access_expression":
|
|
101
|
+
for c in func.children:
|
|
102
|
+
if c.type == "member_binding_expression":
|
|
103
|
+
for cc in c.children:
|
|
104
|
+
if cc.type in ("identifier", "generic_name"):
|
|
105
|
+
return self.method_name(cc)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def arg_exprs(arglist) -> list:
|
|
110
|
+
"""[(expression node, is_named_argument)] for an argument_list node."""
|
|
111
|
+
out = []
|
|
112
|
+
if arglist is None:
|
|
113
|
+
return out
|
|
114
|
+
for a in arglist.named_children:
|
|
115
|
+
if a.type != "argument":
|
|
116
|
+
continue
|
|
117
|
+
named = list(a.named_children)
|
|
118
|
+
out.append((named[-1] if named else None, len(named) > 1))
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------- string evaluation
|
|
122
|
+
def evaluate(self, n, env) -> tuple[str, bool] | None:
|
|
123
|
+
t = n.type
|
|
124
|
+
if t == "string_literal":
|
|
125
|
+
raw = self.text(n)
|
|
126
|
+
if raw.startswith('"""'):
|
|
127
|
+
return raw.strip('"'), True
|
|
128
|
+
body = raw[1:-1] if len(raw) >= 2 else raw
|
|
129
|
+
body = (body.replace('\\"', '"').replace("\\n", "\n").replace("\\r", " ")
|
|
130
|
+
.replace("\\t", " ").replace("\\\\", "\\"))
|
|
131
|
+
return body, True
|
|
132
|
+
if t == "verbatim_string_literal":
|
|
133
|
+
raw = self.text(n)
|
|
134
|
+
return (raw[2:-1].replace('""', '"') if len(raw) >= 3 else raw), True
|
|
135
|
+
if t == "raw_string_literal":
|
|
136
|
+
return self.text(n).strip('"'), True
|
|
137
|
+
if t == "interpolated_string_expression":
|
|
138
|
+
parts, complete = [], True
|
|
139
|
+
for c in n.children:
|
|
140
|
+
if c.type == "interpolation":
|
|
141
|
+
parts.append(HOLE)
|
|
142
|
+
complete = False
|
|
143
|
+
elif c.type in ("string_content", "string_literal_content", "interpolated_string_text",
|
|
144
|
+
"interpolated_verbatim_string_text"):
|
|
145
|
+
parts.append(self.text(c).replace("{{", "{").replace("}}", "}"))
|
|
146
|
+
return "".join(parts), complete
|
|
147
|
+
if t == "parenthesized_expression":
|
|
148
|
+
inner = list(n.named_children)
|
|
149
|
+
return self.evaluate(inner[0], env) if inner else None
|
|
150
|
+
if t == "binary_expression" and any(c.type == "+" for c in n.children):
|
|
151
|
+
kids = n.named_children
|
|
152
|
+
if len(kids) != 2:
|
|
153
|
+
return None
|
|
154
|
+
left, right = self.evaluate(kids[0], env), self.evaluate(kids[1], env)
|
|
155
|
+
if left is None and right is None:
|
|
156
|
+
return None
|
|
157
|
+
lt, lc = left if left else (HOLE, False)
|
|
158
|
+
rt, rc = right if right else (HOLE, False)
|
|
159
|
+
return lt + rt, lc and rc
|
|
160
|
+
if t == "identifier":
|
|
161
|
+
c = env.get(self.text(n))
|
|
162
|
+
return (c.text, c.complete) if c else None
|
|
163
|
+
if t == "member_access_expression":
|
|
164
|
+
name = n.child_by_field_name("name")
|
|
165
|
+
c = self.file_env.get(self.text(name)) if name is not None else None
|
|
166
|
+
return (c.text, c.complete) if c else None
|
|
167
|
+
if t == "invocation_expression":
|
|
168
|
+
func = n.child_by_field_name("function")
|
|
169
|
+
if self.method_name(func) in ("Format", "Concat") and func is not None and "string" in self.text(func).lower():
|
|
170
|
+
args = self.arg_exprs(n.child_by_field_name("arguments"))
|
|
171
|
+
if args and args[0][0] is not None:
|
|
172
|
+
first = self.evaluate(args[0][0], env)
|
|
173
|
+
if first is not None:
|
|
174
|
+
return re.sub(r"\{\d+[^}]*\}", HOLE, first[0]), False
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def _is_stringy(self, n) -> bool:
|
|
178
|
+
if n.type in _STRINGISH:
|
|
179
|
+
return True
|
|
180
|
+
if n.type == "binary_expression" and any(c.type == "+" for c in n.children):
|
|
181
|
+
return any(self._is_stringy(c) for c in n.named_children)
|
|
182
|
+
if n.type == "parenthesized_expression":
|
|
183
|
+
return any(self._is_stringy(c) for c in n.named_children)
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------- traversal
|
|
187
|
+
def run(self, root) -> list[Access]:
|
|
188
|
+
self._collect_file_consts(root)
|
|
189
|
+
env: dict[str, _Cand] = dict(self.file_env)
|
|
190
|
+
stack: list = [("node", root)]
|
|
191
|
+
while stack:
|
|
192
|
+
kind, n = stack.pop()
|
|
193
|
+
if kind == "exit":
|
|
194
|
+
mark, scope_node, saved_env = n
|
|
195
|
+
self._flush(mark, scope_node)
|
|
196
|
+
env = saved_env
|
|
197
|
+
continue
|
|
198
|
+
if n.type in _SCOPES:
|
|
199
|
+
stack.append(("exit", (len(self.cands), n, env)))
|
|
200
|
+
env = dict(self.file_env)
|
|
201
|
+
for c in reversed(n.children):
|
|
202
|
+
stack.append(("node", c))
|
|
203
|
+
continue
|
|
204
|
+
if self._visit(n, env, stack):
|
|
205
|
+
continue
|
|
206
|
+
for c in reversed(n.children):
|
|
207
|
+
stack.append(("node", c))
|
|
208
|
+
self._flush(0, root)
|
|
209
|
+
return self.out
|
|
210
|
+
|
|
211
|
+
def _collect_file_consts(self, root) -> None:
|
|
212
|
+
stack = [root]
|
|
213
|
+
while stack:
|
|
214
|
+
n = stack.pop()
|
|
215
|
+
if n.type == "field_declaration":
|
|
216
|
+
for d in _walk(n, "variable_declarator"):
|
|
217
|
+
ident, init = self._declarator(d)
|
|
218
|
+
if ident and init is not None and self._is_stringy(init):
|
|
219
|
+
r = self.evaluate(init, self.file_env)
|
|
220
|
+
if r:
|
|
221
|
+
c = _Cand(r[0], r[1], self.line(init))
|
|
222
|
+
self.file_env[ident] = c
|
|
223
|
+
self.cands.append(c)
|
|
224
|
+
continue
|
|
225
|
+
if n.type in _SCOPES:
|
|
226
|
+
continue
|
|
227
|
+
stack.extend(n.children)
|
|
228
|
+
|
|
229
|
+
def _declarator(self, d):
|
|
230
|
+
ident, init = None, None
|
|
231
|
+
for c in d.named_children:
|
|
232
|
+
if c.type == "identifier" and ident is None:
|
|
233
|
+
ident = self.text(c)
|
|
234
|
+
else:
|
|
235
|
+
init = c
|
|
236
|
+
return ident, init
|
|
237
|
+
|
|
238
|
+
def _visit(self, n, env, stack) -> bool:
|
|
239
|
+
"""Handle nodes of interest. Returns True when the node's children must not be traversed."""
|
|
240
|
+
t = n.type
|
|
241
|
+
if t == "variable_declarator":
|
|
242
|
+
ident, init = self._declarator(n)
|
|
243
|
+
if ident and init is not None and self._is_stringy(init):
|
|
244
|
+
r = self.evaluate(init, env)
|
|
245
|
+
if r:
|
|
246
|
+
c = _Cand(r[0], r[1], self.line(init))
|
|
247
|
+
env[ident] = c
|
|
248
|
+
self.cands.append(c)
|
|
249
|
+
return True
|
|
250
|
+
return False
|
|
251
|
+
if t == "assignment_expression":
|
|
252
|
+
return self._assignment(n, env, stack)
|
|
253
|
+
if t == "invocation_expression":
|
|
254
|
+
return self._invocation(n, env, stack)
|
|
255
|
+
if t == "object_creation_expression":
|
|
256
|
+
return self._creation(n, env, stack)
|
|
257
|
+
if t == "attribute":
|
|
258
|
+
self._attribute(n)
|
|
259
|
+
return False
|
|
260
|
+
if self._is_stringy(n):
|
|
261
|
+
r = self.evaluate(n, env)
|
|
262
|
+
if r:
|
|
263
|
+
self.cands.append(_Cand(r[0], r[1], self.line(n)))
|
|
264
|
+
return True
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
def _assignment(self, n, env, stack) -> bool:
|
|
268
|
+
kids = n.named_children
|
|
269
|
+
if len(kids) != 2:
|
|
270
|
+
return False
|
|
271
|
+
left, right = kids
|
|
272
|
+
op = next((c.type for c in n.children if c.type in ("=", "+=")), "=")
|
|
273
|
+
if left.type == "member_access_expression" and self.text(left.child_by_field_name("name")) == "CommandText":
|
|
274
|
+
if not self._sink_arg(right, env, stack, first=True):
|
|
275
|
+
if not self._is_stringy(right):
|
|
276
|
+
self._dynamic_site(n)
|
|
277
|
+
stack.append(("node", left))
|
|
278
|
+
return True
|
|
279
|
+
if left.type == "identifier" and self._is_stringy(right):
|
|
280
|
+
name = self.text(left)
|
|
281
|
+
r = self.evaluate(right, env)
|
|
282
|
+
if r is None:
|
|
283
|
+
return False
|
|
284
|
+
cur = env.get(name)
|
|
285
|
+
if op == "+=" and cur is not None:
|
|
286
|
+
cur.text += r[0]
|
|
287
|
+
cur.complete = cur.complete and r[1]
|
|
288
|
+
elif cur is not None and re.search(rf"\b{re.escape(name)}\b", self.text(right)):
|
|
289
|
+
cur.text, cur.complete = r # sql = sql + "..."
|
|
290
|
+
else:
|
|
291
|
+
c = _Cand(r[0], r[1], self.line(right))
|
|
292
|
+
env[name] = c
|
|
293
|
+
self.cands.append(c)
|
|
294
|
+
return True
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
def _sink_arg(self, expr, env, stack, first: bool) -> bool:
|
|
298
|
+
"""An argument handed to a database API. True when it was a statically known string."""
|
|
299
|
+
if expr is None:
|
|
300
|
+
return False
|
|
301
|
+
if expr.type == "identifier":
|
|
302
|
+
c = env.get(self.text(expr))
|
|
303
|
+
if c is not None:
|
|
304
|
+
c.sink = True
|
|
305
|
+
c.proc_ok = c.proc_ok or first
|
|
306
|
+
return True
|
|
307
|
+
elif self._is_stringy(expr) or expr.type in ("member_access_expression", "invocation_expression"):
|
|
308
|
+
r = self.evaluate(expr, env)
|
|
309
|
+
if r is not None:
|
|
310
|
+
self.cands.append(_Cand(r[0], r[1], self.line(expr), sink=True, proc_ok=first))
|
|
311
|
+
return True
|
|
312
|
+
stack.append(("node", expr))
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
def _invocation(self, n, env, stack) -> bool:
|
|
316
|
+
func = n.child_by_field_name("function")
|
|
317
|
+
name = self.method_name(func)
|
|
318
|
+
args = self.arg_exprs(n.child_by_field_name("arguments"))
|
|
319
|
+
if name in ("ToTable", "ToView") and args and args[0][0] is not None and self._is_stringy(args[0][0]):
|
|
320
|
+
r = self.evaluate(args[0][0], env)
|
|
321
|
+
if r and r[1]:
|
|
322
|
+
self._mapping(r[0], n, func)
|
|
323
|
+
if name and _SINK_METHOD.match(name):
|
|
324
|
+
handled_any = False
|
|
325
|
+
for i, (expr, _named) in enumerate(args):
|
|
326
|
+
handled = self._sink_arg(expr, env, stack, first=not handled_any)
|
|
327
|
+
handled_any = handled_any or handled
|
|
328
|
+
if not handled_any and _DAPPER_STRICT.match(name) and args and args[0][0] is not None:
|
|
329
|
+
second = args[1][0] if len(args) > 1 else None
|
|
330
|
+
has_params = second is not None and second.type in (
|
|
331
|
+
"anonymous_object_creation_expression", "object_creation_expression", "identifier")
|
|
332
|
+
if has_params or any(named for _e, named in args):
|
|
333
|
+
self._dynamic_site(n)
|
|
334
|
+
if func is not None:
|
|
335
|
+
stack.append(("node", func))
|
|
336
|
+
return True
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
def _creation(self, n, env, stack) -> bool:
|
|
340
|
+
tnode = n.child_by_field_name("type")
|
|
341
|
+
tname = self.text(tnode) if tnode is not None else ""
|
|
342
|
+
if not _COMMAND_TYPE.search(tname.split(".")[-1].split("<")[0]):
|
|
343
|
+
return False
|
|
344
|
+
args = self.arg_exprs(n.child_by_field_name("arguments"))
|
|
345
|
+
if args and args[0][0] is not None:
|
|
346
|
+
if not self._sink_arg(args[0][0], env, stack, first=True):
|
|
347
|
+
self._dynamic_site(n)
|
|
348
|
+
for expr, _named in args[1:]:
|
|
349
|
+
if expr is not None:
|
|
350
|
+
stack.append(("node", expr))
|
|
351
|
+
for c in n.children:
|
|
352
|
+
if c.type == "initializer_expression":
|
|
353
|
+
stack.append(("node", c))
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
def _dynamic_site(self, n) -> None:
|
|
357
|
+
self.out.append(Access(
|
|
358
|
+
key="?", name="(dynamic)", qualifier="", kind="unknown", access="exec", op="dynamic",
|
|
359
|
+
line=self.line(n), via="csharp_sql", confidence="uncertain", evidence=self.collapsed(n),
|
|
360
|
+
))
|
|
361
|
+
|
|
362
|
+
# ---------------------------------------------------------------- mappings
|
|
363
|
+
def _map_access(self, table: str, line: int, evidence: str, entity: str) -> None:
|
|
364
|
+
parts = [p.strip("[] ") for p in table.split(".")]
|
|
365
|
+
self.out.append(Access(
|
|
366
|
+
key=parts[-1].lower(), name=parts[-1], qualifier=".".join(parts[:-1]), kind="table", access="map",
|
|
367
|
+
op="map", line=line, via="ef_mapping", confidence="confirmed", evidence=evidence, detail=entity,
|
|
368
|
+
))
|
|
369
|
+
|
|
370
|
+
def _mapping(self, table: str, n, func) -> None:
|
|
371
|
+
entity = ""
|
|
372
|
+
node = func.child_by_field_name("expression") if func is not None and func.type == "member_access_expression" else None
|
|
373
|
+
while node is not None and node.type == "invocation_expression":
|
|
374
|
+
f = node.child_by_field_name("function")
|
|
375
|
+
if f is None or f.type != "member_access_expression":
|
|
376
|
+
break
|
|
377
|
+
nm = f.child_by_field_name("name")
|
|
378
|
+
if nm is not None and nm.type == "generic_name" and self.method_name(nm) == "Entity":
|
|
379
|
+
targs = [c for c in nm.children if c.type == "type_argument_list"]
|
|
380
|
+
if targs and targs[0].named_children:
|
|
381
|
+
entity = self.text(targs[0].named_children[0])
|
|
382
|
+
break
|
|
383
|
+
node = f.child_by_field_name("expression")
|
|
384
|
+
self._map_access(table, self.line(n), self.collapsed(n), entity)
|
|
385
|
+
|
|
386
|
+
def _attribute(self, n) -> None:
|
|
387
|
+
name = next((self.text(c) for c in n.named_children if c.type in ("identifier", "qualified_name")), "")
|
|
388
|
+
if name.split(".")[-1] not in ("Table", "TableAttribute"):
|
|
389
|
+
return
|
|
390
|
+
arglist = next((c for c in n.named_children if c.type == "attribute_argument_list"), None)
|
|
391
|
+
if arglist is None or not arglist.named_children:
|
|
392
|
+
return
|
|
393
|
+
first = list(arglist.named_children[0].named_children)
|
|
394
|
+
if not first or first[0].type not in _STRINGISH:
|
|
395
|
+
return
|
|
396
|
+
r = self.evaluate(first[0], {})
|
|
397
|
+
if not r or not r[0]:
|
|
398
|
+
return
|
|
399
|
+
cls = n.parent.parent if n.parent is not None else None
|
|
400
|
+
entity = ""
|
|
401
|
+
if cls is not None:
|
|
402
|
+
entity = next((self.text(c) for c in cls.named_children if c.type == "identifier"), "")
|
|
403
|
+
self._map_access(r[0], self.line(n), self.collapsed(n), entity)
|
|
404
|
+
|
|
405
|
+
# ---------------------------------------------------------------- emission
|
|
406
|
+
def _flush(self, mark: int, scope_node) -> None:
|
|
407
|
+
cands, self.cands = self.cands[mark:], self.cands[:mark]
|
|
408
|
+
has_sp = (scope_node.type in _SCOPES
|
|
409
|
+
and b"StoredProcedure" in self.src[scope_node.start_byte:scope_node.end_byte])
|
|
410
|
+
for c in cands:
|
|
411
|
+
text = c.text.strip()
|
|
412
|
+
if not text:
|
|
413
|
+
continue
|
|
414
|
+
collapsed = " ".join(text.split())[:160]
|
|
415
|
+
if (c.sink and c.proc_ok and c.complete and "\n" not in text and _PROC_NAME.match(text)
|
|
416
|
+
and text.lower() not in _NOT_PROC):
|
|
417
|
+
parts = text.split(".")
|
|
418
|
+
self.out.append(Access(
|
|
419
|
+
key=parts[-1].lower(), name=parts[-1], qualifier=".".join(parts[:-1]), kind="procedure",
|
|
420
|
+
access="exec", op="exec", line=c.line, via="csharp_sp",
|
|
421
|
+
confidence="confirmed" if has_sp else "probable", evidence=collapsed,
|
|
422
|
+
))
|
|
423
|
+
continue
|
|
424
|
+
if not (c.sink or looks_like_statement(text)):
|
|
425
|
+
continue
|
|
426
|
+
for a in extract_sql_accesses(text, c.line):
|
|
427
|
+
a.line = c.line
|
|
428
|
+
a.via = "csharp_sql"
|
|
429
|
+
a.evidence = collapsed
|
|
430
|
+
if a.confidence != "uncertain":
|
|
431
|
+
a.confidence = "confirmed" if (c.sink and c.complete) else "probable"
|
|
432
|
+
self.out.append(a)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def extract_cs_accesses(root, src: bytes, symbols: list) -> list[Access]:
|
|
436
|
+
ex = _Extractor(src, symbols)
|
|
437
|
+
try:
|
|
438
|
+
accesses = ex.run(root)
|
|
439
|
+
except RecursionError:
|
|
440
|
+
return []
|
|
441
|
+
seen, out = set(), []
|
|
442
|
+
for a in accesses:
|
|
443
|
+
sym = ex.locate(a.line)
|
|
444
|
+
a.source_id = sym.id if sym is not None else None
|
|
445
|
+
sig = (a.source_id, a.key, a.access, a.op, a.line, a.via)
|
|
446
|
+
if sig not in seen:
|
|
447
|
+
seen.add(sig)
|
|
448
|
+
out.append(a)
|
|
449
|
+
out.sort(key=lambda a: (a.line, a.access, a.key))
|
|
450
|
+
return out
|