agent-code-guard 0.1.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.
- agent_code_guard/__init__.py +1 -0
- agent_code_guard/analysis/__init__.py +13 -0
- agent_code_guard/analysis/adapters.py +608 -0
- agent_code_guard/analysis/errors.py +13 -0
- agent_code_guard/analysis/facts.py +101 -0
- agent_code_guard/analysis/language_specs.py +82 -0
- agent_code_guard/analysis/pipeline.py +37 -0
- agent_code_guard/analysis/provider.py +45 -0
- agent_code_guard/analysis/regions.py +108 -0
- agent_code_guard/code_guard.py +236 -0
- agent_code_guard/config_validation.py +90 -0
- agent_code_guard/file_selection.py +228 -0
- agent_code_guard/guards/__init__.py +1 -0
- agent_code_guard/guards/callable_size.py +79 -0
- agent_code_guard/guards/complexity.py +94 -0
- agent_code_guard/guards/loc.py +235 -0
- agent_code_guard/guards/markdown_document_size.py +66 -0
- agent_code_guard/guards/markdown_section_size.py +66 -0
- agent_code_guard/guards/nesting.py +109 -0
- agent_code_guard/markdown/__init__.py +6 -0
- agent_code_guard/markdown/facts.py +27 -0
- agent_code_guard/markdown/scanner.py +109 -0
- agent_code_guard/path_matching.py +25 -0
- agent_code_guard/reporting.py +11 -0
- agent_code_guard/result_model.py +128 -0
- agent_code_guard/skill_distribution.py +96 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/LICENSE.txt +21 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/SKILL.md +138 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/agents/openai.yaml +8 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/callable-size-policy.md +39 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/complexity-policy.md +39 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/loc-policy.md +40 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/markdown-size-policy.md +16 -0
- agent_code_guard-0.1.0.data/data/share/agent-code-guard/skill/references/nesting-policy.md +37 -0
- agent_code_guard-0.1.0.dist-info/METADATA +206 -0
- agent_code_guard-0.1.0.dist-info/RECORD +40 -0
- agent_code_guard-0.1.0.dist-info/WHEEL +5 -0
- agent_code_guard-0.1.0.dist-info/entry_points.txt +2 -0
- agent_code_guard-0.1.0.dist-info/licenses/LICENSE +21 -0
- agent_code_guard-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent Code Guard production package."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Production source/container and syntax-fact pipeline."""
|
|
2
|
+
|
|
3
|
+
from .errors import AnalysisError, ProviderUnavailableError, SyntaxAnalysisError
|
|
4
|
+
from .facts import AnalysisFacts, CallableFact, CallableKey, ControlFlowFact, DecisionFact, FileFacts, SourcePoint, SourceRange
|
|
5
|
+
from .pipeline import analyze_files
|
|
6
|
+
from .provider import TreeSitterProvider
|
|
7
|
+
from .regions import ExecutableRegion, is_applicable
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AnalysisError", "AnalysisFacts", "CallableFact", "CallableKey", "ControlFlowFact", "DecisionFact",
|
|
11
|
+
"ExecutableRegion", "FileFacts", "ProviderUnavailableError", "SourcePoint", "SourceRange",
|
|
12
|
+
"SyntaxAnalysisError", "TreeSitterProvider", "analyze_files", "is_applicable",
|
|
13
|
+
]
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""Language-specific Tree-sitter extraction into normalized immutable facts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterator
|
|
6
|
+
|
|
7
|
+
from .facts import CallableFact, CallableKey, ControlFlowFact, DecisionFact, SourceRange
|
|
8
|
+
from .language_specs import (
|
|
9
|
+
CALLABLE_TYPES, CONTROL_CATEGORIES, CONTROL_TYPES, DECISION_CATEGORIES, DECISION_TYPES,
|
|
10
|
+
OPAQUE_LAMBDA_TYPES,
|
|
11
|
+
)
|
|
12
|
+
from .regions import ExecutableRegion
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_facts(root, region: ExecutableRegion) -> tuple[tuple[CallableFact, ...], tuple[ControlFlowFact, ...], tuple[DecisionFact, ...]]:
|
|
16
|
+
nodes = [node for node in _walk(root) if node.type in CALLABLE_TYPES[region.language] and _has_body(node, region.language)]
|
|
17
|
+
identities = {_node_key(node): _identity(node, region) for node in nodes}
|
|
18
|
+
ranges = {_node_key(node): _callable_range(node, region) for node in nodes}
|
|
19
|
+
keys = {
|
|
20
|
+
node_key: CallableKey(region.original_path, region.language, identity, ranges[node_key])
|
|
21
|
+
for node_key, identity in identities.items()
|
|
22
|
+
}
|
|
23
|
+
callables: list[CallableFact] = []
|
|
24
|
+
controls: list[ControlFlowFact] = []
|
|
25
|
+
decisions: list[DecisionFact] = []
|
|
26
|
+
for node in nodes:
|
|
27
|
+
identity = identities[_node_key(node)]
|
|
28
|
+
parent_node = next((ancestor for ancestor in _ancestors(node) if _node_key(ancestor) in identities), None)
|
|
29
|
+
if parent_node is None:
|
|
30
|
+
containing = [candidate for candidate in nodes if candidate is not node
|
|
31
|
+
and ranges[_node_key(candidate)].start.byte_offset <= ranges[_node_key(node)].start.byte_offset
|
|
32
|
+
and ranges[_node_key(candidate)].end.byte_offset >= ranges[_node_key(node)].end.byte_offset]
|
|
33
|
+
parent_node = min(containing, key=lambda candidate: ranges[_node_key(candidate)].physical_loc, default=None)
|
|
34
|
+
node_key = _node_key(node)
|
|
35
|
+
parent_key = keys.get(_node_key(parent_node)) if parent_node is not None else None
|
|
36
|
+
callables.append(CallableFact(
|
|
37
|
+
region.original_path, region.language, identity, ranges[node_key],
|
|
38
|
+
identities.get(_node_key(parent_node)) if parent_node is not None else None,
|
|
39
|
+
"callback" if _is_anonymous_callable(node, region) else ("nested" if parent_node else "callable"),
|
|
40
|
+
keys[node_key], parent_key,
|
|
41
|
+
))
|
|
42
|
+
extracted_controls, extracted_decisions = _structural_facts(node, keys[node_key], region)
|
|
43
|
+
controls.extend(extracted_controls)
|
|
44
|
+
decisions.extend(extracted_decisions)
|
|
45
|
+
key = lambda fact: (fact.source_range.start.byte_offset, fact.source_range.end.byte_offset)
|
|
46
|
+
return tuple(sorted(callables, key=key)), tuple(sorted(controls, key=key)), tuple(sorted(decisions, key=key))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _structural_facts(callable_node, callable_key: CallableKey, region: ExecutableRegion):
|
|
50
|
+
controls: list[ControlFlowFact] = []
|
|
51
|
+
decisions: list[DecisionFact] = []
|
|
52
|
+
language = region.language
|
|
53
|
+
|
|
54
|
+
def visit(node, parent_control: SourceRange | None) -> None:
|
|
55
|
+
for child in node.named_children:
|
|
56
|
+
if child.type in CALLABLE_TYPES[language] or child.type in OPAQUE_LAMBDA_TYPES[language]:
|
|
57
|
+
continue
|
|
58
|
+
child_range = region.original_range(child)
|
|
59
|
+
next_parent = parent_control
|
|
60
|
+
if child.type in CONTROL_TYPES[language] and _is_meaningful_control(child, language):
|
|
61
|
+
increases = not (child.type in {"elif_clause", "else_if_clause"} or _is_else_if(child, language))
|
|
62
|
+
controls.append(ControlFlowFact(
|
|
63
|
+
callable_key.identity, callable_key, _control_category(child.type, language), child.type,
|
|
64
|
+
child_range, parent_control, increases,
|
|
65
|
+
))
|
|
66
|
+
if increases:
|
|
67
|
+
next_parent = child_range
|
|
68
|
+
if child.type in DECISION_TYPES[language] and not _is_default_branch(child, region.source, language):
|
|
69
|
+
decisions.append(DecisionFact(
|
|
70
|
+
callable_key.identity, callable_key, DECISION_CATEGORIES.get(child.type, child.type), child.type, child_range,
|
|
71
|
+
))
|
|
72
|
+
for provider_kind, arm_range in _extra_switch_arm_ranges(child, language, region):
|
|
73
|
+
decisions.append(DecisionFact(callable_key.identity, callable_key, "switch_arm", provider_kind, arm_range))
|
|
74
|
+
for arm_range in _php_switch_arm_ranges(child, language, region):
|
|
75
|
+
decisions.append(DecisionFact(
|
|
76
|
+
callable_key.identity, callable_key, "switch_arm", "case_statement", arm_range,
|
|
77
|
+
))
|
|
78
|
+
guard = _pattern_guard(child, language)
|
|
79
|
+
if guard is not None:
|
|
80
|
+
decisions.append(DecisionFact(callable_key.identity, callable_key, "pattern_guard", guard.type,
|
|
81
|
+
region.original_range(guard)))
|
|
82
|
+
visit(child, next_parent)
|
|
83
|
+
|
|
84
|
+
for structural_root in _structural_roots(callable_node, language):
|
|
85
|
+
visit(structural_root, None)
|
|
86
|
+
return controls, decisions
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _walk(node) -> Iterator:
|
|
90
|
+
yield node
|
|
91
|
+
for child in node.named_children:
|
|
92
|
+
yield from _walk(child)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _node_key(node) -> tuple[str, int, int]:
|
|
96
|
+
return node.type, node.start_byte, node.end_byte
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _has_body(node, language: str) -> bool:
|
|
100
|
+
if language in {"typescript", "tsx"} and node.type in {"function_declaration", "method_definition"}:
|
|
101
|
+
return node.child_by_field_name("body") is not None
|
|
102
|
+
if language == "swift" and node.type == "protocol_function_declaration":
|
|
103
|
+
return any(child.type == "statements" for child in node.named_children)
|
|
104
|
+
if language == "dart" and node.type in {"function_signature", "method_signature"}:
|
|
105
|
+
return _dart_body(node) is not None and not any(parent.type == "lambda_expression" for parent in _ancestors(node))
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _range_end_node(node, language: str):
|
|
110
|
+
return _dart_body(node) if language == "dart" and _dart_body(node) is not None else node
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _callable_range(node, region: ExecutableRegion) -> SourceRange:
|
|
114
|
+
"""Snapshot provider points once before mapping them to original source."""
|
|
115
|
+
start_row, start_column = _range_start_node(node, region.language).start_point
|
|
116
|
+
end_row, end_column = _range_end_node(node, region.language).end_point
|
|
117
|
+
return SourceRange(
|
|
118
|
+
region.original_point(start_row, start_column),
|
|
119
|
+
region.original_point(end_row, end_column),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _structural_roots(node, language: str):
|
|
124
|
+
body = _dart_body(node) if language == "dart" else None
|
|
125
|
+
return (body,) if body is not None else (node,)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _dart_body(node):
|
|
129
|
+
if node.type not in {"function_signature", "method_signature"}:
|
|
130
|
+
return None
|
|
131
|
+
sibling = node.next_named_sibling
|
|
132
|
+
return sibling if sibling is not None and sibling.type == "function_body" else None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _range_start_node(node, language: str):
|
|
136
|
+
if language == "python" and node.parent and node.parent.type == "decorated_definition":
|
|
137
|
+
return node.parent
|
|
138
|
+
if language in {"javascript", "typescript", "tsx"} and node.type in {"arrow_function", "function_expression"}:
|
|
139
|
+
declarator = _ancestor(node, "variable_declarator")
|
|
140
|
+
if declarator and declarator.child_by_field_name("value") == node:
|
|
141
|
+
return declarator.parent if declarator.parent and declarator.parent.type in {"lexical_declaration", "variable_declaration"} else declarator
|
|
142
|
+
if language in {"typescript", "tsx"} and node.type == "method_definition":
|
|
143
|
+
first = node
|
|
144
|
+
previous = node.prev_named_sibling
|
|
145
|
+
while previous and previous.type == "decorator":
|
|
146
|
+
first, previous = previous, previous.prev_named_sibling
|
|
147
|
+
return first
|
|
148
|
+
if language == "cpp" and node.parent and node.parent.type == "template_declaration":
|
|
149
|
+
return node.parent
|
|
150
|
+
if language in {"cpp", "php", "swift", "dart", "rust"} and _is_closure(node, language):
|
|
151
|
+
owner = _assigned_closure_owner(node, language)
|
|
152
|
+
if owner is not None:
|
|
153
|
+
return owner
|
|
154
|
+
if language == "swift" and node.type == "protocol_function_declaration" and node.prev_named_sibling:
|
|
155
|
+
previous = node.prev_named_sibling
|
|
156
|
+
if previous.type == "protocol_function_declaration" and previous.child_by_field_name("name") is not None:
|
|
157
|
+
return previous
|
|
158
|
+
return node
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _identity(node, region: ExecutableRegion) -> str:
|
|
162
|
+
if region.language in {"javascript", "typescript", "tsx"}:
|
|
163
|
+
return _javascript_identity(node, region)
|
|
164
|
+
if region.language in {"cpp", "rust", "php", "swift", "dart"}:
|
|
165
|
+
return _second_wave_identity(node, region)
|
|
166
|
+
if node.type in MAINSTREAM_LAMBDA_TYPES[region.language]:
|
|
167
|
+
return _mainstream_lambda_identity(node, region)
|
|
168
|
+
source, language = region.source, region.language
|
|
169
|
+
parts = [_name(node, language, source)]
|
|
170
|
+
owner_types = {
|
|
171
|
+
"python": {"class_definition", "function_definition"}, "go": set(),
|
|
172
|
+
"kotlin": {"class_declaration", "object_declaration", "function_declaration"},
|
|
173
|
+
"csharp": {"namespace_declaration", "file_scoped_namespace_declaration", "class_declaration", "struct_declaration", "record_declaration", "method_declaration", "constructor_declaration", "local_function_statement"},
|
|
174
|
+
"java": {"class_declaration", "record_declaration", "enum_declaration", "method_declaration", "constructor_declaration"},
|
|
175
|
+
}[language]
|
|
176
|
+
for current in _ancestors(node):
|
|
177
|
+
if current.type in owner_types:
|
|
178
|
+
name = _name_node(current, language)
|
|
179
|
+
if name:
|
|
180
|
+
parts.append(_text(name, source))
|
|
181
|
+
if language == "go":
|
|
182
|
+
receiver_type = _go_receiver_type(node, source)
|
|
183
|
+
if receiver_type:
|
|
184
|
+
parts.append(receiver_type)
|
|
185
|
+
parts.append(region.original_path.stem if language == "python" else _package_or_namespace(node, language, source))
|
|
186
|
+
return ".".join(reversed([part for part in parts if part]))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _javascript_identity(node, region: ExecutableRegion) -> str:
|
|
190
|
+
source = region.source
|
|
191
|
+
name = node.child_by_field_name("name")
|
|
192
|
+
if node.type in {"arrow_function", "function_expression"}:
|
|
193
|
+
declarator = _ancestor(node, "variable_declarator")
|
|
194
|
+
if declarator and declarator.child_by_field_name("value") == node:
|
|
195
|
+
name = declarator.child_by_field_name("name")
|
|
196
|
+
parts = [_text(name, source) if name else _callback_name(node, region)]
|
|
197
|
+
for current in _ancestors(node):
|
|
198
|
+
owner = None
|
|
199
|
+
if current.type == "class_declaration":
|
|
200
|
+
owner = current.child_by_field_name("name")
|
|
201
|
+
elif current.type == "method_definition" and current is not node:
|
|
202
|
+
owner = current.child_by_field_name("name")
|
|
203
|
+
elif current.type in {"function_declaration", "arrow_function", "function_expression"} and current is not node:
|
|
204
|
+
lexical = _javascript_lexical_name(current, source)
|
|
205
|
+
if lexical:
|
|
206
|
+
parts.append(lexical)
|
|
207
|
+
if owner:
|
|
208
|
+
parts.append(_text(owner, source))
|
|
209
|
+
if node.type == "method_definition" and not any(value.type == "class_declaration" for value in _ancestors(node)):
|
|
210
|
+
object_name = _object_assignment_name(node, source)
|
|
211
|
+
if object_name:
|
|
212
|
+
parts.append(object_name)
|
|
213
|
+
parts.append(region.original_path.stem)
|
|
214
|
+
return ".".join(reversed(parts))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _name(node, language: str, source: bytes) -> str:
|
|
218
|
+
name = _name_node(node, language)
|
|
219
|
+
if name:
|
|
220
|
+
return _text(name, source)
|
|
221
|
+
if language in {"kotlin", "csharp", "java"} and "constructor" in node.type:
|
|
222
|
+
for owner in _ancestors(node):
|
|
223
|
+
if owner.type in {"class_declaration", "object_declaration", "struct_declaration", "record_declaration", "enum_declaration"}:
|
|
224
|
+
owner_name = _name_node(owner, language)
|
|
225
|
+
if owner_name:
|
|
226
|
+
return _text(owner_name, source)
|
|
227
|
+
return "<anonymous>"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _name_node(node, language: str):
|
|
231
|
+
name = node.child_by_field_name("name")
|
|
232
|
+
if name is None and language == "kotlin":
|
|
233
|
+
name = next((child for child in node.named_children if child.type in {"simple_identifier", "type_identifier"}), None)
|
|
234
|
+
return name
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _go_receiver_type(method_node, source: bytes) -> str | None:
|
|
238
|
+
receiver = method_node.child_by_field_name("receiver")
|
|
239
|
+
if receiver is None:
|
|
240
|
+
return None
|
|
241
|
+
words = _text(receiver, source).replace("(", "").replace(")", "").replace("*", "").split()
|
|
242
|
+
return words[-1] if words else "receiver"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _second_wave_identity(node, region: ExecutableRegion) -> str:
|
|
246
|
+
language, source = region.language, region.source
|
|
247
|
+
name = _second_wave_name(node, language, source)
|
|
248
|
+
parts = [name or _callback_name(node, region)]
|
|
249
|
+
owner_types = {
|
|
250
|
+
"cpp": {"namespace_definition", "class_specifier", "struct_specifier", "union_specifier", "function_definition"},
|
|
251
|
+
"rust": {"trait_item", "impl_item", "function_item"},
|
|
252
|
+
"php": {"namespace_definition", "class_declaration", "trait_declaration", "interface_declaration", "function_definition", "method_declaration"},
|
|
253
|
+
"swift": {"class_declaration", "struct_declaration", "protocol_declaration", "function_declaration", "init_declaration"},
|
|
254
|
+
"dart": {"class_definition", "function_signature", "constructor_signature", "lambda_expression"},
|
|
255
|
+
}[language]
|
|
256
|
+
for current in _ancestors(node):
|
|
257
|
+
if current.type not in owner_types:
|
|
258
|
+
continue
|
|
259
|
+
owner = _second_wave_name(current, language, source)
|
|
260
|
+
if owner:
|
|
261
|
+
parts.append(owner)
|
|
262
|
+
parts.append(region.original_path.stem)
|
|
263
|
+
return ".".join(reversed(parts))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _second_wave_name(node, language: str, source: bytes) -> str | None:
|
|
267
|
+
name = node.child_by_field_name("name")
|
|
268
|
+
if language == "cpp" and node.type == "function_definition":
|
|
269
|
+
declarator = node.child_by_field_name("declarator")
|
|
270
|
+
name = _deep_named_child(declarator, {"identifier", "field_identifier", "destructor_name", "operator_name"})
|
|
271
|
+
elif language == "cpp" and node.type == "lambda_expression":
|
|
272
|
+
name = _assigned_name(node, language)
|
|
273
|
+
elif language == "rust" and node.type == "closure_expression":
|
|
274
|
+
name = _assigned_name(node, language)
|
|
275
|
+
elif language == "php" and node.type in {"arrow_function", "anonymous_function"}:
|
|
276
|
+
name = _assigned_name(node, language)
|
|
277
|
+
elif language == "swift" and node.type == "init_declaration":
|
|
278
|
+
return "init"
|
|
279
|
+
elif language == "swift" and node.type == "protocol_function_declaration" and name is None:
|
|
280
|
+
previous = node.prev_named_sibling
|
|
281
|
+
name = previous.child_by_field_name("name") if previous is not None else None
|
|
282
|
+
elif language == "swift" and node.type == "lambda_literal":
|
|
283
|
+
name = _assigned_name(node, language)
|
|
284
|
+
elif language == "dart" and node.type in {"function_expression", "lambda_expression"}:
|
|
285
|
+
signature = next((child for child in node.named_children if child.type == "function_signature"), None)
|
|
286
|
+
name = signature.child_by_field_name("name") if signature is not None else _assigned_name(node, language)
|
|
287
|
+
elif language == "dart" and node.type == "method_signature":
|
|
288
|
+
return _dart_method_name(node, source)
|
|
289
|
+
elif language == "dart" and node.type == "class_definition":
|
|
290
|
+
name = node.child_by_field_name("name")
|
|
291
|
+
elif language == "rust" and node.type == "impl_item":
|
|
292
|
+
name = node.child_by_field_name("type")
|
|
293
|
+
if name is None and language == "swift" and node.type == "class_declaration":
|
|
294
|
+
name = next((child for child in node.named_children if child.type in {"type_identifier", "user_type"}), None)
|
|
295
|
+
return _text(name, source).lstrip("$") if name is not None else None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _dart_method_name(method_signature, source: bytes) -> str | None:
|
|
299
|
+
signature = next((child for child in method_signature.named_children if child.type in {
|
|
300
|
+
"function_signature", "constructor_signature", "factory_constructor_signature",
|
|
301
|
+
}), None)
|
|
302
|
+
if signature is None:
|
|
303
|
+
return None
|
|
304
|
+
if signature.type in {"constructor_signature", "factory_constructor_signature"}:
|
|
305
|
+
return _dart_constructor_name(signature, source)
|
|
306
|
+
name = signature.child_by_field_name("name")
|
|
307
|
+
return _text(name, source).lstrip("$") if name is not None else None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _dart_constructor_name(signature, source: bytes) -> str | None:
|
|
311
|
+
identifiers = [child for child in signature.named_children if child.type == "identifier"]
|
|
312
|
+
return ".".join(_text(child, source).lstrip("$") for child in identifiers) or None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _deep_named_child(node, types: set[str]):
|
|
316
|
+
if node is None:
|
|
317
|
+
return None
|
|
318
|
+
if node.type in types:
|
|
319
|
+
return node
|
|
320
|
+
for child in node.named_children:
|
|
321
|
+
found = _deep_named_child(child, types)
|
|
322
|
+
if found is not None:
|
|
323
|
+
return found
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _is_closure(node, language: str) -> bool:
|
|
328
|
+
return node.type in {
|
|
329
|
+
"cpp": {"lambda_expression"}, "rust": {"closure_expression"},
|
|
330
|
+
"php": {"arrow_function", "anonymous_function"},
|
|
331
|
+
"swift": {"lambda_literal"}, "dart": {"function_expression", "lambda_expression"},
|
|
332
|
+
}.get(language, set())
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _assigned_name(node, language: str):
|
|
336
|
+
owner = _assigned_closure_owner(node, language)
|
|
337
|
+
if owner is None:
|
|
338
|
+
return None
|
|
339
|
+
candidates = {
|
|
340
|
+
"cpp": {"identifier"}, "rust": {"identifier"}, "php": {"variable_name"},
|
|
341
|
+
"swift": {"pattern"}, "dart": {"identifier"},
|
|
342
|
+
}[language]
|
|
343
|
+
return _deep_named_child(owner, candidates)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _assigned_closure_owner(node, language: str):
|
|
347
|
+
if language == "php":
|
|
348
|
+
assignment = _ancestor(node, "assignment_expression")
|
|
349
|
+
if assignment is None or assignment.child_by_field_name("right") != node:
|
|
350
|
+
return None
|
|
351
|
+
return assignment.parent if assignment.parent and assignment.parent.type == "expression_statement" else assignment
|
|
352
|
+
owner_types = {
|
|
353
|
+
"cpp": {"declaration"}, "rust": {"let_declaration"},
|
|
354
|
+
"swift": {"property_declaration"}, "dart": {"local_variable_declaration"},
|
|
355
|
+
}[language]
|
|
356
|
+
current = node.parent
|
|
357
|
+
while current and current.type not in CALLABLE_TYPES[language]:
|
|
358
|
+
if current.type in owner_types:
|
|
359
|
+
return current
|
|
360
|
+
current = current.parent
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _package_or_namespace(node, language: str, source: bytes) -> str:
|
|
365
|
+
root = node
|
|
366
|
+
while root.parent:
|
|
367
|
+
root = root.parent
|
|
368
|
+
types = {"go": {"package_clause"}, "kotlin": {"package_header"}, "csharp": {"file_scoped_namespace_declaration"}, "java": {"package_declaration"}}.get(language, set())
|
|
369
|
+
for child in root.named_children:
|
|
370
|
+
if child.type in types:
|
|
371
|
+
return _text(child, source).replace("package", "", 1).replace("namespace", "", 1).strip().rstrip(";")
|
|
372
|
+
return ""
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _javascript_lexical_name(node, source: bytes) -> str | None:
|
|
376
|
+
name = node.child_by_field_name("name")
|
|
377
|
+
if name:
|
|
378
|
+
return _text(name, source)
|
|
379
|
+
declarator = _ancestor(node, "variable_declarator")
|
|
380
|
+
if declarator and declarator.child_by_field_name("value") == node:
|
|
381
|
+
target = declarator.child_by_field_name("name")
|
|
382
|
+
if target and target.type in {"identifier", "property_identifier"}:
|
|
383
|
+
return _text(target, source)
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _callback_name(node, region: ExecutableRegion) -> str:
|
|
388
|
+
row, column = node.start_point
|
|
389
|
+
point = region.original_point(row, column)
|
|
390
|
+
return f"<callback@{point.line}:{point.byte_column}>"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
MAINSTREAM_LAMBDA_TYPES = {
|
|
394
|
+
"python": {"lambda"}, "go": {"func_literal"},
|
|
395
|
+
"kotlin": {"lambda_literal", "anonymous_function"},
|
|
396
|
+
"csharp": {"lambda_expression", "anonymous_method_expression"},
|
|
397
|
+
"java": {"lambda_expression"},
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _mainstream_lambda_identity(node, region: ExecutableRegion) -> str:
|
|
402
|
+
parts = [_callback_name(node, region)]
|
|
403
|
+
for current in _ancestors(node):
|
|
404
|
+
if current.type in MAINSTREAM_LAMBDA_TYPES[region.language]:
|
|
405
|
+
parts.append(_callback_name(current, region))
|
|
406
|
+
elif current.type in CALLABLE_TYPES[region.language]:
|
|
407
|
+
name = _name_node(current, region.language)
|
|
408
|
+
if name is not None:
|
|
409
|
+
parts.append(_text(name, region.source))
|
|
410
|
+
if region.language == "go" and current.type == "method_declaration":
|
|
411
|
+
receiver_type = _go_receiver_type(current, region.source)
|
|
412
|
+
if receiver_type:
|
|
413
|
+
parts.append(receiver_type)
|
|
414
|
+
elif current.type in {"class_definition", "class_declaration", "object_declaration", "struct_declaration", "record_declaration", "enum_declaration"}:
|
|
415
|
+
name = _name_node(current, region.language)
|
|
416
|
+
if name is not None:
|
|
417
|
+
parts.append(_text(name, region.source))
|
|
418
|
+
parts.append(region.original_path.stem if region.language == "python" else _package_or_namespace(node, region.language, region.source))
|
|
419
|
+
return ".".join(reversed([part for part in parts if part]))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _is_anonymous_callable(node, region: ExecutableRegion) -> bool:
|
|
423
|
+
if region.language in {"javascript", "typescript", "tsx"}:
|
|
424
|
+
return _javascript_lexical_name(node, region.source) is None
|
|
425
|
+
if node.type in MAINSTREAM_LAMBDA_TYPES.get(region.language, set()):
|
|
426
|
+
return True
|
|
427
|
+
return _is_closure(node, region.language) and _second_wave_name(node, region.language, region.source) is None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _object_assignment_name(node, source: bytes) -> str | None:
|
|
431
|
+
object_node = _ancestor(node, "object")
|
|
432
|
+
declarator = _ancestor(object_node, "variable_declarator") if object_node else None
|
|
433
|
+
target = declarator.child_by_field_name("name") if declarator else None
|
|
434
|
+
return _text(target, source) if target and target.type == "identifier" else None
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _ancestor(node, node_type: str):
|
|
438
|
+
current = node.parent if node else None
|
|
439
|
+
while current:
|
|
440
|
+
if current.type == node_type:
|
|
441
|
+
return current
|
|
442
|
+
current = current.parent
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _ancestors(node) -> Iterator:
|
|
447
|
+
current = node.parent
|
|
448
|
+
while current:
|
|
449
|
+
yield current
|
|
450
|
+
current = current.parent
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _text(node, source: bytes) -> str:
|
|
454
|
+
return source[node.start_byte:node.end_byte].decode("utf-8")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _is_else_if(node, language: str) -> bool:
|
|
458
|
+
if node.type not in {"if_statement", "if_expression"}:
|
|
459
|
+
return False
|
|
460
|
+
parent = node.parent
|
|
461
|
+
if language == "kotlin":
|
|
462
|
+
return bool(parent and parent.type == "control_structure_body" and parent.parent and parent.parent.type == "if_expression")
|
|
463
|
+
if language in {"javascript", "typescript", "tsx"}:
|
|
464
|
+
return bool(parent and parent.type == "else_clause")
|
|
465
|
+
if language == "php":
|
|
466
|
+
return bool(parent and parent.type == "else_if_clause")
|
|
467
|
+
if language == "swift":
|
|
468
|
+
return bool(parent and parent.type == "if_statement")
|
|
469
|
+
if language == "rust":
|
|
470
|
+
return bool(parent and parent.type == "else_clause")
|
|
471
|
+
return bool(parent and parent.type == "if_statement" and parent.child_by_field_name("alternative") == node)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _is_default_branch(node, source: bytes, language: str | None = None) -> bool:
|
|
475
|
+
if node.type == "else_if_clause":
|
|
476
|
+
return False
|
|
477
|
+
if language == "python" and node.type == "case_clause" and node.child_by_field_name("guard") is not None:
|
|
478
|
+
return False
|
|
479
|
+
return _text(node, source).lstrip().startswith(("default", "else", "case _", "case var _", "_ ->", "_ =>"))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _control_category(provider_kind: str, language: str) -> str:
|
|
483
|
+
if language == "swift" and provider_kind == "do_statement":
|
|
484
|
+
return "exception"
|
|
485
|
+
return CONTROL_CATEGORIES.get(provider_kind, provider_kind)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _is_meaningful_control(node, language: str) -> bool:
|
|
489
|
+
if language == "swift" and node.type == "do_statement":
|
|
490
|
+
return any(child.type == "catch_block" for child in node.named_children)
|
|
491
|
+
return True
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _extra_switch_arm_ranges(node, language: str, region: ExecutableRegion) -> tuple[tuple[str, SourceRange], ...]:
|
|
495
|
+
if language == "java" and node.type == "switch_rule":
|
|
496
|
+
return () if _is_default_branch(node, region.source) else ((node.type, region.original_range(node)),)
|
|
497
|
+
|
|
498
|
+
clauses: tuple
|
|
499
|
+
provider_kind: str
|
|
500
|
+
if language == "cpp" and node.type == "compound_statement" and node.parent.type == "switch_statement":
|
|
501
|
+
clauses = tuple(_cpp_switch_clauses(node))
|
|
502
|
+
provider_kind = "case_statement"
|
|
503
|
+
elif language == "csharp" and node.type == "switch_body":
|
|
504
|
+
clauses = tuple(child for child in node.named_children if child.type == "switch_section")
|
|
505
|
+
provider_kind = "switch_section"
|
|
506
|
+
elif language == "java" and node.type == "switch_block":
|
|
507
|
+
clauses = tuple(child for child in node.named_children if child.type == "switch_block_statement_group")
|
|
508
|
+
provider_kind = "switch_block_statement_group"
|
|
509
|
+
elif language in {"javascript", "typescript", "tsx"} and node.type == "switch_body":
|
|
510
|
+
clauses = tuple(child for child in node.named_children if child.type in {"switch_case", "switch_default"})
|
|
511
|
+
provider_kind = "switch_case"
|
|
512
|
+
else:
|
|
513
|
+
return ()
|
|
514
|
+
|
|
515
|
+
ranges: list[SourceRange] = []
|
|
516
|
+
pending_case = None
|
|
517
|
+
for index, clause in enumerate(clauses):
|
|
518
|
+
non_default = not _is_default_branch(clause, region.source)
|
|
519
|
+
next_clause = clauses[index + 1] if language == "cpp" and index + 1 < len(clauses) else None
|
|
520
|
+
if not _classic_switch_clause_has_body(clause, next_clause):
|
|
521
|
+
if non_default and pending_case is None:
|
|
522
|
+
pending_case = clause
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
representative = pending_case or (clause if non_default else None)
|
|
526
|
+
if representative is not None:
|
|
527
|
+
ranges.append(region.original_range(representative))
|
|
528
|
+
pending_case = None
|
|
529
|
+
return tuple((provider_kind, arm_range) for arm_range in ranges)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _cpp_switch_clauses(node) -> Iterator:
|
|
533
|
+
for child in node.named_children:
|
|
534
|
+
if child.type == "switch_statement":
|
|
535
|
+
continue
|
|
536
|
+
if child.type == "case_statement":
|
|
537
|
+
yield child
|
|
538
|
+
yield from _cpp_switch_clauses(child)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _classic_switch_clause_has_body(clause, next_clause=None) -> bool:
|
|
542
|
+
colon = next((child for child in clause.children if child.type == ":"), None)
|
|
543
|
+
if colon is None:
|
|
544
|
+
return False
|
|
545
|
+
for child in clause.children:
|
|
546
|
+
if not child.is_named or child.start_byte < colon.end_byte or child.type in {"comment", "empty_statement"}:
|
|
547
|
+
continue
|
|
548
|
+
if next_clause is not None and child.start_byte < next_clause.start_byte < child.end_byte:
|
|
549
|
+
if child.type == "compound_statement" or child.type.startswith("preproc_"):
|
|
550
|
+
return _cpp_wrapper_has_executable_before(child, next_clause.start_byte)
|
|
551
|
+
return True
|
|
552
|
+
return True
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _cpp_wrapper_has_executable_before(node, limit: int) -> bool:
|
|
557
|
+
preprocessor_wrapper = node.type.startswith("preproc_")
|
|
558
|
+
for child in node.named_children:
|
|
559
|
+
if child.start_byte >= limit or child.type in {"case_statement", "comment", "empty_statement"}:
|
|
560
|
+
continue
|
|
561
|
+
if child.type == "compound_statement" or child.type.startswith("preproc_"):
|
|
562
|
+
if _cpp_wrapper_has_executable_before(child, limit):
|
|
563
|
+
return True
|
|
564
|
+
elif not preprocessor_wrapper or child.type.endswith(("_statement", "_declaration")) or child.type == "declaration":
|
|
565
|
+
return True
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _php_switch_arm_ranges(node, language: str, region: ExecutableRegion) -> tuple[SourceRange, ...]:
|
|
570
|
+
"""Normalize PHP case-label groups into executable non-default arms."""
|
|
571
|
+
if language != "php" or node.type != "switch_block":
|
|
572
|
+
return ()
|
|
573
|
+
|
|
574
|
+
ranges: list[SourceRange] = []
|
|
575
|
+
pending_case = None
|
|
576
|
+
for clause in (child for child in node.named_children
|
|
577
|
+
if child.type in {"case_statement", "default_statement"}):
|
|
578
|
+
value = clause.child_by_field_name("value")
|
|
579
|
+
body = [child for child in clause.named_children
|
|
580
|
+
if child != value and child.type not in {"comment", "empty_statement"}]
|
|
581
|
+
if not body:
|
|
582
|
+
if clause.type == "case_statement" and pending_case is None:
|
|
583
|
+
pending_case = clause
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
representative = pending_case or (clause if clause.type == "case_statement" else None)
|
|
587
|
+
if representative is not None:
|
|
588
|
+
ranges.append(region.original_range(representative))
|
|
589
|
+
pending_case = None
|
|
590
|
+
return tuple(ranges)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _pattern_guard(node, language: str):
|
|
594
|
+
if language == "python" and node.type == "case_clause":
|
|
595
|
+
guard = node.child_by_field_name("guard")
|
|
596
|
+
return guard.named_children[0] if guard is not None and guard.named_children else None
|
|
597
|
+
if language == "csharp" and node.type == "switch_expression_arm":
|
|
598
|
+
clause = next((child for child in node.named_children if child.type == "when_clause"), None)
|
|
599
|
+
return clause.named_children[0] if clause is not None and clause.named_children else None
|
|
600
|
+
if language == "rust" and node.type == "match_arm":
|
|
601
|
+
pattern = node.child_by_field_name("pattern")
|
|
602
|
+
return pattern.child_by_field_name("condition") if pattern is not None else None
|
|
603
|
+
if language == "swift" and node.type == "switch_entry":
|
|
604
|
+
children = node.named_children
|
|
605
|
+
for index, child in enumerate(children):
|
|
606
|
+
if child.type == "where_keyword" and index + 1 < len(children):
|
|
607
|
+
return children[index + 1]
|
|
608
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Deterministic failures raised by the production syntax pipeline."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AnalysisError(RuntimeError):
|
|
5
|
+
"""Base error suitable for Code Guard's existing exit-3 boundary."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProviderUnavailableError(AnalysisError):
|
|
9
|
+
"""The configured parser provider or a required grammar is unavailable."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SyntaxAnalysisError(AnalysisError):
|
|
13
|
+
"""A supported source/container cannot produce authoritative facts."""
|