code-atlas-forge 1.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.
- code_atlas/__init__.py +6 -0
- code_atlas/__main__.py +6 -0
- code_atlas/api/__init__.py +5 -0
- code_atlas/api/handlers.py +1514 -0
- code_atlas/api/server.py +394 -0
- code_atlas/cli.py +2084 -0
- code_atlas/config.py +149 -0
- code_atlas/context/__init__.py +66 -0
- code_atlas/context/contracts.py +277 -0
- code_atlas/context/domain.py +760 -0
- code_atlas/context/engine.py +442 -0
- code_atlas/context/expand.py +133 -0
- code_atlas/context/renderers.py +71 -0
- code_atlas/context/tokenizer.py +37 -0
- code_atlas/contracts/__init__.py +38 -0
- code_atlas/contracts/enums.py +67 -0
- code_atlas/contracts/models.py +175 -0
- code_atlas/contracts/ports.py +70 -0
- code_atlas/discovery/__init__.py +25 -0
- code_atlas/discovery/gitignore.py +119 -0
- code_atlas/discovery/identity.py +52 -0
- code_atlas/discovery/models.py +204 -0
- code_atlas/discovery/scanner.py +495 -0
- code_atlas/domains/__init__.py +107 -0
- code_atlas/domains/api/__init__.py +28 -0
- code_atlas/domains/api/adapter.py +405 -0
- code_atlas/domains/api/common.py +308 -0
- code_atlas/domains/api/consumers.py +224 -0
- code_atlas/domains/api/emit.py +231 -0
- code_atlas/domains/api/openapi.py +398 -0
- code_atlas/domains/api/routes.py +582 -0
- code_atlas/domains/api/yaml_subset.py +683 -0
- code_atlas/domains/aws/__init__.py +28 -0
- code_atlas/domains/aws/adapter.py +382 -0
- code_atlas/domains/aws/common.py +261 -0
- code_atlas/domains/aws/configs.py +1164 -0
- code_atlas/domains/aws/emit.py +411 -0
- code_atlas/domains/aws/pyfiles.py +2220 -0
- code_atlas/domains/aws/sqlbridge.py +448 -0
- code_atlas/domains/cicd/__init__.py +50 -0
- code_atlas/domains/cicd/adapter.py +762 -0
- code_atlas/domains/cicd/classify.py +136 -0
- code_atlas/domains/cicd/workflow.py +504 -0
- code_atlas/domains/cicd/yamlmini.py +946 -0
- code_atlas/domains/cli_surface.py +739 -0
- code_atlas/domains/contracts.py +274 -0
- code_atlas/domains/crossresolve.py +1208 -0
- code_atlas/domains/evidence.py +57 -0
- code_atlas/domains/identity.py +149 -0
- code_atlas/domains/model.py +202 -0
- code_atlas/domains/registry.py +250 -0
- code_atlas/domains/resolution.py +65 -0
- code_atlas/domains/spark/__init__.py +31 -0
- code_atlas/domains/spark/adapter.py +146 -0
- code_atlas/domains/spark/emit.py +344 -0
- code_atlas/domains/spark/extract.py +108 -0
- code_atlas/domains/spark/sqllit.py +192 -0
- code_atlas/domains/spark/walk.py +1943 -0
- code_atlas/domains/sql/__init__.py +30 -0
- code_atlas/domains/sql/adapter.py +382 -0
- code_atlas/domains/sql/extract.py +1271 -0
- code_atlas/domains/sql/nodes.py +264 -0
- code_atlas/domains/sql/parser.py +1510 -0
- code_atlas/domains/sql/tokens.py +318 -0
- code_atlas/domains/summarize.py +126 -0
- code_atlas/domains/terraform/__init__.py +20 -0
- code_atlas/domains/terraform/adapter.py +127 -0
- code_atlas/domains/terraform/catalog.py +43 -0
- code_atlas/domains/terraform/extract.py +1277 -0
- code_atlas/domains/terraform/hcl.py +538 -0
- code_atlas/domains/terraform/refs.py +181 -0
- code_atlas/domains/terraform/tfjson.py +163 -0
- code_atlas/domains/terraform/tfvars.py +98 -0
- code_atlas/domains/validation.py +120 -0
- code_atlas/env_override.py +27 -0
- code_atlas/eval/__init__.py +26 -0
- code_atlas/eval/domain_eval.py +681 -0
- code_atlas/eval/ground_truth.py +688 -0
- code_atlas/eval/impact_truth.py +758 -0
- code_atlas/eval/metrics.py +178 -0
- code_atlas/eval/pack_value.py +610 -0
- code_atlas/federation/__init__.py +72 -0
- code_atlas/federation/federation.py +676 -0
- code_atlas/federation/model.py +355 -0
- code_atlas/federation/store.py +194 -0
- code_atlas/gitbin.py +48 -0
- code_atlas/graph/__init__.py +20 -0
- code_atlas/graph/builder.py +378 -0
- code_atlas/graph/identity.py +54 -0
- code_atlas/graph/queries.py +47 -0
- code_atlas/graph/validation.py +92 -0
- code_atlas/impact/__init__.py +37 -0
- code_atlas/impact/blast_radius.py +171 -0
- code_atlas/impact/contract_analyzer.py +104 -0
- code_atlas/impact/contracts.py +315 -0
- code_atlas/impact/diff_reader.py +142 -0
- code_atlas/impact/domain_traversal.py +768 -0
- code_atlas/impact/engine.py +392 -0
- code_atlas/impact/packs.py +128 -0
- code_atlas/impact/report.py +136 -0
- code_atlas/impact/revision_binding.py +220 -0
- code_atlas/impact/risk_classifier.py +83 -0
- code_atlas/impact/snapshot_resolver.py +136 -0
- code_atlas/impact/symbol_diff.py +210 -0
- code_atlas/impact/test_selector.py +166 -0
- code_atlas/indexing/__init__.py +5 -0
- code_atlas/indexing/cache.py +396 -0
- code_atlas/indexing/change_detector.py +278 -0
- code_atlas/indexing/coordinator.py +1032 -0
- code_atlas/indexing/dependency_planner.py +268 -0
- code_atlas/indexing/persist.py +436 -0
- code_atlas/indexing/reuse.py +85 -0
- code_atlas/install/__init__.py +7 -0
- code_atlas/install/cli.py +168 -0
- code_atlas/install/lifecycle.py +286 -0
- code_atlas/mcp.py +674 -0
- code_atlas/observability/__init__.py +19 -0
- code_atlas/observability/logging.py +199 -0
- code_atlas/parsers/__init__.py +43 -0
- code_atlas/parsers/contracts.py +209 -0
- code_atlas/parsers/engine.py +125 -0
- code_atlas/parsers/languages/__init__.py +1 -0
- code_atlas/parsers/languages/python.py +391 -0
- code_atlas/parsers/languages/tsx.py +28 -0
- code_atlas/parsers/languages/typescript.py +909 -0
- code_atlas/parsers/registry.py +51 -0
- code_atlas/parsers/tree_sitter_runtime.py +118 -0
- code_atlas/resolvers/__init__.py +33 -0
- code_atlas/resolvers/bootstrap.py +246 -0
- code_atlas/resolvers/capabilities.py +33 -0
- code_atlas/resolvers/contracts.py +133 -0
- code_atlas/resolvers/coordinator.py +96 -0
- code_atlas/resolvers/enrichment.py +318 -0
- code_atlas/resolvers/lsp/__init__.py +26 -0
- code_atlas/resolvers/lsp/adapter.py +256 -0
- code_atlas/resolvers/lsp/lifecycle.py +251 -0
- code_atlas/resolvers/lsp/spawn.py +92 -0
- code_atlas/resolvers/lsp/transport.py +240 -0
- code_atlas/resolvers/registry.py +33 -0
- code_atlas/resolvers/scip/__init__.py +49 -0
- code_atlas/resolvers/scip/decoder.py +216 -0
- code_atlas/resolvers/scip/importer.py +248 -0
- code_atlas/resolvers/scip/loader.py +90 -0
- code_atlas/resolvers/scip/mapper.py +271 -0
- code_atlas/resolvers/scip/provenance.py +211 -0
- code_atlas/resolvers/scip/resolver.py +289 -0
- code_atlas/resolvers/scip/validation.py +101 -0
- code_atlas/resolvers/syntactic/__init__.py +8 -0
- code_atlas/resolvers/syntactic/resolver.py +254 -0
- code_atlas/search/__init__.py +32 -0
- code_atlas/search/contracts.py +176 -0
- code_atlas/search/domain.py +561 -0
- code_atlas/search/engine.py +530 -0
- code_atlas/search/federated.py +276 -0
- code_atlas/search/graph.py +282 -0
- code_atlas/search/hybrid.py +151 -0
- code_atlas/search/intent.py +48 -0
- code_atlas/search/lexical.py +176 -0
- code_atlas/search/refs.py +359 -0
- code_atlas/search/structural.py +505 -0
- code_atlas/search/tokenizer.py +43 -0
- code_atlas/search/vector/__init__.py +35 -0
- code_atlas/search/vector/contracts.py +76 -0
- code_atlas/search/vector/embeddings.py +96 -0
- code_atlas/search/vector/index.py +169 -0
- code_atlas/search/vector/provider.py +50 -0
- code_atlas/storage/__init__.py +14 -0
- code_atlas/storage/db.py +47 -0
- code_atlas/storage/migrations.py +552 -0
- code_atlas/storage/store.py +1045 -0
- code_atlas/ui/__init__.py +29 -0
- code_atlas/ui/i18n.py +143 -0
- code_atlas/ui/kit.py +689 -0
- code_atlas/ui/wizard.py +317 -0
- code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
- code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
- code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
- code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
- code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
- code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
- code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""Parser HCL minimalista e seguro (lf-0115, Cycle 7 Phase 7).
|
|
2
|
+
|
|
3
|
+
Hand-rolled, stdlib only — **nunca** executa ``terraform``, nunca lê
|
|
4
|
+
state, nunca faz plan/apply. Cobre o subconjunto estrutural de HCL
|
|
5
|
+
necessário ao adapter:
|
|
6
|
+
|
|
7
|
+
- blocos ``resource``/``module``/``data``/``variable``/``locals``/
|
|
8
|
+
``output``/``provider``/``terraform`` com labels quoted;
|
|
9
|
+
- atributos ``name = expr`` com strings, heredocs (``<<EOF`` /
|
|
10
|
+
``<<-EOF``), listas/mapas multi-linha, chamadas de função e
|
|
11
|
+
interpolações ``${...}``;
|
|
12
|
+
- comentários ``#``, ``//`` e ``/* */``;
|
|
13
|
+
- blocos aninhados arbitrários (``lifecycle {}``, ``statement {}``…).
|
|
14
|
+
|
|
15
|
+
Semântica de falha: **diagnóstico, nunca exceção** — conteúdo malformado
|
|
16
|
+
produz o que foi parseado (parcial) + ``HclDiagnostic`` com posição.
|
|
17
|
+
Spans seguem a convenção de ``SourceSpan``: byte offsets UTF-8, linha
|
|
18
|
+
1-based, coluna em bytes 0-based.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import bisect
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
from code_atlas.parsers.contracts import SourceSpan
|
|
27
|
+
|
|
28
|
+
_IDENT_START = frozenset(
|
|
29
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"
|
|
30
|
+
)
|
|
31
|
+
_IDENT = frozenset(
|
|
32
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789-"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class HclDiagnostic:
|
|
38
|
+
"""Problema estrutural do parser HCL — honesto, com posição."""
|
|
39
|
+
|
|
40
|
+
code: str # parse-error | unterminated-block | unterminated-string…
|
|
41
|
+
message: str
|
|
42
|
+
line: int = 0 # 1-based; 0 = desconhecida
|
|
43
|
+
col: int = 0 # byte col 0-based
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class HclAttribute:
|
|
48
|
+
"""Atributo ``name = expr`` num corpo de bloco (ou top-level tfvars)."""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
value: str # texto bruto da expressão (não avaliado)
|
|
52
|
+
span: SourceSpan # span de ``name = expr``
|
|
53
|
+
value_span: SourceSpan
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class HclBlock:
|
|
58
|
+
"""Bloco HCL ``type "label"… { body }`` com estrutura aninhada."""
|
|
59
|
+
|
|
60
|
+
type: str
|
|
61
|
+
labels: tuple[str, ...] = ()
|
|
62
|
+
attributes: tuple[HclAttribute, ...] = ()
|
|
63
|
+
children: tuple[HclBlock, ...] = ()
|
|
64
|
+
body: str = "" # texto bruto entre ``{`` e ``}`` (sem as chaves)
|
|
65
|
+
body_start: int = 0 # byte offset do início de ``body``
|
|
66
|
+
span: SourceSpan | None = None # bloco inteiro
|
|
67
|
+
header_span: SourceSpan | None = None # ``type "l1" "l2"`` sem ``{``
|
|
68
|
+
|
|
69
|
+
def attribute(self, name: str) -> HclAttribute | None:
|
|
70
|
+
"""Primeiro atributo top-level com ``name`` (ordem declarada)."""
|
|
71
|
+
for a in self.attributes:
|
|
72
|
+
if a.name == name:
|
|
73
|
+
return a
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class HclFile:
|
|
79
|
+
"""Saída do parser: itens top-level + diagnósticos parciais."""
|
|
80
|
+
|
|
81
|
+
blocks: tuple[HclBlock, ...] = ()
|
|
82
|
+
attributes: tuple[HclAttribute, ...] = () # top-level (tfvars)
|
|
83
|
+
diagnostics: tuple[HclDiagnostic, ...] = ()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _ParseError(Exception):
|
|
87
|
+
"""Erro interno de parse — o caller resincroniza, nunca propaga."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _Parser:
|
|
91
|
+
"""Cursor sobre ``text`` com posição→(byte,line,col) pré-computada."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, text: str) -> None:
|
|
94
|
+
self.text = text
|
|
95
|
+
self.n = len(text)
|
|
96
|
+
self.pos = 0
|
|
97
|
+
self.diags: list[HclDiagnostic] = []
|
|
98
|
+
# offset de byte UTF-8 por índice de char (posições são chars;
|
|
99
|
+
# SourceSpan exige bytes — converte uma vez, O(n)).
|
|
100
|
+
self._byte: list[int] = [0] * (self.n + 1)
|
|
101
|
+
off = 0
|
|
102
|
+
for i, ch in enumerate(text):
|
|
103
|
+
self._byte[i] = off
|
|
104
|
+
off += len(ch.encode("utf-8"))
|
|
105
|
+
self._byte[self.n] = off
|
|
106
|
+
self._lines: list[int] = [0]
|
|
107
|
+
for i, ch in enumerate(text):
|
|
108
|
+
if ch == "\n":
|
|
109
|
+
self._lines.append(i + 1)
|
|
110
|
+
|
|
111
|
+
# --- posição ------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _lc(self, char_pos: int) -> tuple[int, int, int]:
|
|
114
|
+
"""``(byte_offset, line 1-based, col bytes 0-based)``."""
|
|
115
|
+
cp = min(max(char_pos, 0), self.n)
|
|
116
|
+
byte = self._byte[cp]
|
|
117
|
+
li = bisect.bisect_right(self._lines, cp) - 1
|
|
118
|
+
line_start = self._lines[li]
|
|
119
|
+
return byte, li + 1, byte - self._byte[line_start]
|
|
120
|
+
|
|
121
|
+
def span(self, start: int, end: int) -> SourceSpan:
|
|
122
|
+
sb, sl, sc = self._lc(start)
|
|
123
|
+
eb, el, ec = self._lc(end)
|
|
124
|
+
return SourceSpan(
|
|
125
|
+
start_byte=sb, end_byte=eb,
|
|
126
|
+
start_line=sl, start_col=sc, end_line=el, end_col=ec,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def diag(self, code: str, message: str, pos: int) -> None:
|
|
130
|
+
_, line, col = self._lc(pos)
|
|
131
|
+
self.diags.append(
|
|
132
|
+
HclDiagnostic(code=code, message=message, line=line, col=col)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# --- trivia -------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def skip_trivia(self) -> None:
|
|
138
|
+
"""Consome whitespace e comentários ``#``/``//``/``/* */``."""
|
|
139
|
+
while self.pos < self.n:
|
|
140
|
+
c = self.text[self.pos]
|
|
141
|
+
if c in " \t\r\n":
|
|
142
|
+
self.pos += 1
|
|
143
|
+
continue
|
|
144
|
+
if c == "#":
|
|
145
|
+
self._skip_line_comment()
|
|
146
|
+
continue
|
|
147
|
+
if c == "/" and self.pos + 1 < self.n:
|
|
148
|
+
nxt = self.text[self.pos + 1]
|
|
149
|
+
if nxt == "/":
|
|
150
|
+
self._skip_line_comment()
|
|
151
|
+
continue
|
|
152
|
+
if nxt == "*":
|
|
153
|
+
end = self.text.find("*/", self.pos + 2)
|
|
154
|
+
if end < 0:
|
|
155
|
+
self.diag(
|
|
156
|
+
"unterminated-comment",
|
|
157
|
+
"block comment never closed", self.pos,
|
|
158
|
+
)
|
|
159
|
+
self.pos = self.n
|
|
160
|
+
return
|
|
161
|
+
self.pos = end + 2
|
|
162
|
+
continue
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
def _skip_line_comment(self) -> None:
|
|
166
|
+
nl = self.text.find("\n", self.pos)
|
|
167
|
+
self.pos = self.n if nl < 0 else nl + 1
|
|
168
|
+
|
|
169
|
+
# --- strings / heredocs -------------------------------------------
|
|
170
|
+
|
|
171
|
+
def _skip_string(self) -> None:
|
|
172
|
+
"""Consome uma string ``"…"`` a partir de ``pos`` (na aspa).
|
|
173
|
+
|
|
174
|
+
Lida com escapes ``\\"`` e interpolações ``${…}`` balanceadas —
|
|
175
|
+
strings dentro da interpolação são reconhecidas recursivamente.
|
|
176
|
+
"""
|
|
177
|
+
start = self.pos
|
|
178
|
+
self.pos += 1 # opening quote
|
|
179
|
+
while self.pos < self.n:
|
|
180
|
+
c = self.text[self.pos]
|
|
181
|
+
if c == "\\":
|
|
182
|
+
self.pos += 2
|
|
183
|
+
continue
|
|
184
|
+
if c == '"':
|
|
185
|
+
self.pos += 1
|
|
186
|
+
return
|
|
187
|
+
if c == "$" and self.pos + 1 < self.n and self.text[self.pos + 1] == "{":
|
|
188
|
+
self._skip_interpolation()
|
|
189
|
+
continue
|
|
190
|
+
self.pos += 1
|
|
191
|
+
self.diag("unterminated-string", "string literal never closed", start)
|
|
192
|
+
|
|
193
|
+
def _skip_interpolation(self) -> None:
|
|
194
|
+
"""Consome ``${…}`` — braces balanceados, strings internas ok."""
|
|
195
|
+
start = self.pos
|
|
196
|
+
self.pos += 2 # consume '${'
|
|
197
|
+
depth = 1
|
|
198
|
+
while self.pos < self.n and depth:
|
|
199
|
+
c = self.text[self.pos]
|
|
200
|
+
if c == "{":
|
|
201
|
+
depth += 1
|
|
202
|
+
elif c == "}":
|
|
203
|
+
depth -= 1
|
|
204
|
+
elif c == '"':
|
|
205
|
+
self._skip_string()
|
|
206
|
+
continue
|
|
207
|
+
elif c == "#":
|
|
208
|
+
self._skip_line_comment()
|
|
209
|
+
continue
|
|
210
|
+
self.pos += 1
|
|
211
|
+
if depth:
|
|
212
|
+
self.diag(
|
|
213
|
+
"unterminated-interpolation",
|
|
214
|
+
"${…} never closed", start,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _heredoc_tag(self) -> tuple[str, int] | None:
|
|
218
|
+
"""Tag de ``<<TAG``/``<<-TAG``/``<<"TAG"`` → ``(tag, end)``."""
|
|
219
|
+
p = self.pos
|
|
220
|
+
if self.text.startswith("<<-", p):
|
|
221
|
+
p += 3
|
|
222
|
+
elif self.text.startswith("<<", p):
|
|
223
|
+
p += 2
|
|
224
|
+
else:
|
|
225
|
+
return None
|
|
226
|
+
q = p
|
|
227
|
+
quoted = q < self.n and self.text[q] == '"'
|
|
228
|
+
if quoted:
|
|
229
|
+
q += 1
|
|
230
|
+
tstart = q
|
|
231
|
+
while q < self.n and self.text[q] in _IDENT:
|
|
232
|
+
q += 1
|
|
233
|
+
if q == tstart:
|
|
234
|
+
return None # '<<' sem tag — não é heredoc
|
|
235
|
+
if quoted:
|
|
236
|
+
if q < self.n and self.text[q] == '"':
|
|
237
|
+
q += 1
|
|
238
|
+
else:
|
|
239
|
+
return None
|
|
240
|
+
tag = self.text[tstart:q - (1 if quoted else 0)].strip()
|
|
241
|
+
return (tag, q) if tag else None
|
|
242
|
+
|
|
243
|
+
def _consume_heredoc_body(self, tag: str, line_start: int) -> None:
|
|
244
|
+
"""Avança linha a linha até o terminador ``tag``."""
|
|
245
|
+
while line_start <= self.n:
|
|
246
|
+
line_end = self.text.find("\n", line_start)
|
|
247
|
+
if line_end < 0:
|
|
248
|
+
line_end = self.n
|
|
249
|
+
if self.text[line_start:line_end].strip() == tag:
|
|
250
|
+
self.pos = line_end + 1 if line_end < self.n else self.n
|
|
251
|
+
return
|
|
252
|
+
if line_end >= self.n:
|
|
253
|
+
break
|
|
254
|
+
line_start = line_end + 1
|
|
255
|
+
self.diag(
|
|
256
|
+
"unterminated-heredoc",
|
|
257
|
+
f"heredoc terminator {tag!r} not found", self.pos,
|
|
258
|
+
)
|
|
259
|
+
self.pos = self.n
|
|
260
|
+
|
|
261
|
+
def _try_heredoc(self) -> bool:
|
|
262
|
+
"""Se ``pos`` inicia ``<<TAG``/``<<-TAG``, consome o heredoc."""
|
|
263
|
+
res = self._heredoc_tag()
|
|
264
|
+
if res is None:
|
|
265
|
+
return False
|
|
266
|
+
tag, q = res
|
|
267
|
+
# consome até o fim da linha de abertura
|
|
268
|
+
nl = self.text.find("\n", q)
|
|
269
|
+
if nl < 0:
|
|
270
|
+
self.diag(
|
|
271
|
+
"unterminated-heredoc",
|
|
272
|
+
f"heredoc {tag!r} opens at EOF", self.pos,
|
|
273
|
+
)
|
|
274
|
+
self.pos = self.n
|
|
275
|
+
return True
|
|
276
|
+
self._consume_heredoc_body(tag, nl + 1)
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
# --- expressões -----------------------------------------------------
|
|
280
|
+
|
|
281
|
+
def read_expression(self) -> tuple[str, int, int]:
|
|
282
|
+
"""Lê uma expressão até newline/``}`` em profundidade zero.
|
|
283
|
+
|
|
284
|
+
Retorna ``(texto, start, end)`` em posições de char. Erros
|
|
285
|
+
viram diagnósticos — a expressão termina no melhor ponto
|
|
286
|
+
conhecido (parcial, nunca exceção).
|
|
287
|
+
"""
|
|
288
|
+
start = self.pos
|
|
289
|
+
depth = 0
|
|
290
|
+
while self.pos < self.n:
|
|
291
|
+
c = self.text[self.pos]
|
|
292
|
+
if depth == 0 and c in ("\n", "}"):
|
|
293
|
+
break # newline ou '}' em depth 0 encerram a expressão
|
|
294
|
+
if c == '"':
|
|
295
|
+
self._skip_string()
|
|
296
|
+
continue
|
|
297
|
+
if c == "<" and self._try_heredoc():
|
|
298
|
+
continue
|
|
299
|
+
if self._is_comment_start():
|
|
300
|
+
if depth == 0:
|
|
301
|
+
break # comentário é trivia — encerra a expressão
|
|
302
|
+
self._skip_comment()
|
|
303
|
+
continue
|
|
304
|
+
depth = self._expr_delimiter(c, depth)
|
|
305
|
+
self.pos += 1
|
|
306
|
+
if depth != 0:
|
|
307
|
+
self.diag(
|
|
308
|
+
"unbalanced-delimiter",
|
|
309
|
+
f"expression ended with depth {depth}", start,
|
|
310
|
+
)
|
|
311
|
+
# whitespace final é trivia (ex.: antes de comentário) — não faz
|
|
312
|
+
# parte do valor bruto.
|
|
313
|
+
raw = self.text[start:self.pos].rstrip()
|
|
314
|
+
return raw, start, start + len(raw)
|
|
315
|
+
|
|
316
|
+
def _is_comment_start(self) -> bool:
|
|
317
|
+
c = self.text[self.pos]
|
|
318
|
+
if c == "#":
|
|
319
|
+
return True
|
|
320
|
+
return (
|
|
321
|
+
c == "/"
|
|
322
|
+
and self.pos + 1 < self.n
|
|
323
|
+
and self.text[self.pos + 1] in "/*"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _skip_comment(self) -> None:
|
|
327
|
+
"""``pos`` em início de comentário ``#``/``//``/``/* */``."""
|
|
328
|
+
if (
|
|
329
|
+
self.text[self.pos] == "/"
|
|
330
|
+
and self.pos + 1 < self.n
|
|
331
|
+
and self.text[self.pos + 1] == "*"
|
|
332
|
+
):
|
|
333
|
+
end = self.text.find("*/", self.pos + 2)
|
|
334
|
+
if end < 0:
|
|
335
|
+
self.diag(
|
|
336
|
+
"unterminated-comment",
|
|
337
|
+
"block comment never closed", self.pos,
|
|
338
|
+
)
|
|
339
|
+
self.pos = self.n
|
|
340
|
+
return
|
|
341
|
+
self.pos = end + 2
|
|
342
|
+
return
|
|
343
|
+
self._skip_line_comment()
|
|
344
|
+
|
|
345
|
+
def _expr_delimiter(self, c: str, depth: int) -> int:
|
|
346
|
+
"""Bracket ``([{``/``)]}`` → novo depth (diag em desequilíbrio)."""
|
|
347
|
+
if c in "([{":
|
|
348
|
+
return depth + 1
|
|
349
|
+
if c in ")]}":
|
|
350
|
+
if depth == 0:
|
|
351
|
+
self.diag(
|
|
352
|
+
"unbalanced-delimiter",
|
|
353
|
+
f"unexpected {c!r} at depth 0", self.pos,
|
|
354
|
+
)
|
|
355
|
+
return depth
|
|
356
|
+
return depth - 1
|
|
357
|
+
return depth
|
|
358
|
+
|
|
359
|
+
# --- tokens --------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def _read_ident(self) -> tuple[str, int, int] | None:
|
|
362
|
+
"""Identificador ``[A-Za-z_][\\w-]*``; ``None`` sem consumir."""
|
|
363
|
+
if self.pos >= self.n or self.text[self.pos] not in _IDENT_START:
|
|
364
|
+
return None
|
|
365
|
+
start = self.pos
|
|
366
|
+
self.pos += 1
|
|
367
|
+
while self.pos < self.n and self.text[self.pos] in _IDENT:
|
|
368
|
+
self.pos += 1
|
|
369
|
+
return self.text[start:self.pos], start, self.pos
|
|
370
|
+
|
|
371
|
+
def _read_label(self) -> tuple[str, int, int] | None:
|
|
372
|
+
"""Label de bloco: string quoted ou identificador bare."""
|
|
373
|
+
self.skip_trivia()
|
|
374
|
+
if self.pos >= self.n:
|
|
375
|
+
return None
|
|
376
|
+
start = self.pos
|
|
377
|
+
if self.text[self.pos] == '"':
|
|
378
|
+
self._skip_string()
|
|
379
|
+
raw = self.text[start:self.pos]
|
|
380
|
+
# remove aspas externas sem interpretar escapes — labels
|
|
381
|
+
# interpoladas são inválidas em HCL de qualquer forma
|
|
382
|
+
return raw[1:-1], start, self.pos
|
|
383
|
+
tok = self._read_ident()
|
|
384
|
+
if tok is not None:
|
|
385
|
+
return tok
|
|
386
|
+
# label numérico (HCL antigo permite ``module 0 {…}``)
|
|
387
|
+
dstart = self.pos
|
|
388
|
+
while self.pos < self.n and self.text[self.pos].isdigit():
|
|
389
|
+
self.pos += 1
|
|
390
|
+
if self.pos > dstart:
|
|
391
|
+
return self.text[dstart:self.pos], dstart, self.pos
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
# --- corpo / bloco ---------------------------------------------------
|
|
395
|
+
|
|
396
|
+
def _parse_body(self, body_char_start: int) -> tuple[
|
|
397
|
+
tuple[HclAttribute, ...], tuple[HclBlock, ...], int
|
|
398
|
+
]:
|
|
399
|
+
"""Parse até o ``}`` de fechamento; retorna attrs, filhos, end."""
|
|
400
|
+
attrs: list[HclAttribute] = []
|
|
401
|
+
children: list[HclBlock] = []
|
|
402
|
+
while True:
|
|
403
|
+
self.skip_trivia()
|
|
404
|
+
if self.pos >= self.n:
|
|
405
|
+
self.diag(
|
|
406
|
+
"unterminated-block",
|
|
407
|
+
"block body reached EOF without '}'", self.pos,
|
|
408
|
+
)
|
|
409
|
+
return tuple(attrs), tuple(children), self.pos
|
|
410
|
+
if self.text[self.pos] == "}":
|
|
411
|
+
self.pos += 1
|
|
412
|
+
return tuple(attrs), tuple(children), self.pos
|
|
413
|
+
item = self._parse_item()
|
|
414
|
+
if isinstance(item, HclAttribute):
|
|
415
|
+
attrs.append(item)
|
|
416
|
+
elif isinstance(item, HclBlock):
|
|
417
|
+
children.append(item)
|
|
418
|
+
# item None = resync já aplicado por _parse_item
|
|
419
|
+
|
|
420
|
+
def _read_block_labels(
|
|
421
|
+
self, name: str, item_start: int
|
|
422
|
+
) -> tuple[str, ...] | None:
|
|
423
|
+
"""Labels até ``{`` — ``None`` quando o header não fecha."""
|
|
424
|
+
labels: list[str] = []
|
|
425
|
+
while True:
|
|
426
|
+
self.skip_trivia()
|
|
427
|
+
if self.pos >= self.n:
|
|
428
|
+
self.diag(
|
|
429
|
+
"unterminated-block",
|
|
430
|
+
f"block {name!r} header reached EOF", item_start,
|
|
431
|
+
)
|
|
432
|
+
return None
|
|
433
|
+
if self.text[self.pos] == "{":
|
|
434
|
+
return tuple(labels)
|
|
435
|
+
lab = self._read_label()
|
|
436
|
+
if lab is not None:
|
|
437
|
+
labels.append(lab[0])
|
|
438
|
+
continue
|
|
439
|
+
self.diag(
|
|
440
|
+
"parse-error",
|
|
441
|
+
f"expected label or '{{' after {name!r}", self.pos,
|
|
442
|
+
)
|
|
443
|
+
# resync: pula até próximo '{' ou newline
|
|
444
|
+
while self.pos < self.n and self.text[self.pos] not in "{\n":
|
|
445
|
+
self.pos += 1
|
|
446
|
+
if self.pos >= self.n or self.text[self.pos] == "\n":
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
def _parse_item(self) -> HclAttribute | HclBlock | None:
|
|
450
|
+
"""Um item de corpo: atributo ``name = expr`` ou bloco aninhado."""
|
|
451
|
+
item_start = self.pos
|
|
452
|
+
tok = self._read_ident()
|
|
453
|
+
if tok is None:
|
|
454
|
+
self.diag(
|
|
455
|
+
"parse-error",
|
|
456
|
+
f"unexpected character {self.text[self.pos]!r}", self.pos,
|
|
457
|
+
)
|
|
458
|
+
# resync: avança até newline — progresso garantido
|
|
459
|
+
self._skip_line_comment()
|
|
460
|
+
if self.pos <= item_start:
|
|
461
|
+
self.pos = item_start + 1
|
|
462
|
+
return None
|
|
463
|
+
name, name_start, _name_end = tok
|
|
464
|
+
self.skip_trivia()
|
|
465
|
+
if (
|
|
466
|
+
self.pos < self.n
|
|
467
|
+
and self.text[self.pos] == "="
|
|
468
|
+
and (self.pos + 1 >= self.n or self.text[self.pos + 1] != "=")
|
|
469
|
+
):
|
|
470
|
+
self.pos += 1
|
|
471
|
+
self.skip_trivia()
|
|
472
|
+
value, vstart, vend = self.read_expression()
|
|
473
|
+
return HclAttribute(
|
|
474
|
+
name=name, value=value,
|
|
475
|
+
span=self.span(name_start, vend),
|
|
476
|
+
value_span=self.span(vstart, vend),
|
|
477
|
+
)
|
|
478
|
+
labels = self._read_block_labels(name, item_start)
|
|
479
|
+
if labels is None:
|
|
480
|
+
return None
|
|
481
|
+
header_end = self.pos
|
|
482
|
+
self.pos += 1 # consume '{'
|
|
483
|
+
body_char_start = self.pos
|
|
484
|
+
attrs, children, body_end = self._parse_body(body_char_start)
|
|
485
|
+
# ``body_end`` inclui o ``}`` de fechamento quando presente
|
|
486
|
+
inner_end = (
|
|
487
|
+
body_end - 1
|
|
488
|
+
if body_end > body_char_start and self.text[body_end - 1] == "}"
|
|
489
|
+
else body_end
|
|
490
|
+
)
|
|
491
|
+
return HclBlock(
|
|
492
|
+
type=name, labels=labels,
|
|
493
|
+
attributes=attrs, children=children,
|
|
494
|
+
body=self.text[body_char_start:inner_end],
|
|
495
|
+
body_start=self._byte[body_char_start],
|
|
496
|
+
span=self.span(item_start, body_end),
|
|
497
|
+
header_span=self.span(item_start, header_end),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def parse(self) -> HclFile:
|
|
501
|
+
"""Top-level: mistura de blocos e atributos (tfvars)."""
|
|
502
|
+
blocks: list[HclBlock] = []
|
|
503
|
+
attrs: list[HclAttribute] = []
|
|
504
|
+
while True:
|
|
505
|
+
self.skip_trivia()
|
|
506
|
+
if self.pos >= self.n:
|
|
507
|
+
break
|
|
508
|
+
if self.text[self.pos] == "}":
|
|
509
|
+
self.diag("stray-brace", "'}' without open block", self.pos)
|
|
510
|
+
self.pos += 1
|
|
511
|
+
continue
|
|
512
|
+
item_start = self.pos
|
|
513
|
+
item = self._parse_item()
|
|
514
|
+
if isinstance(item, HclBlock):
|
|
515
|
+
blocks.append(item)
|
|
516
|
+
elif isinstance(item, HclAttribute):
|
|
517
|
+
attrs.append(item)
|
|
518
|
+
if self.pos <= item_start: # guard anti-loop infinito
|
|
519
|
+
self.pos = item_start + 1
|
|
520
|
+
return HclFile(
|
|
521
|
+
blocks=tuple(blocks), attributes=tuple(attrs),
|
|
522
|
+
diagnostics=tuple(self.diags),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def parse_hcl(text: str) -> HclFile:
|
|
527
|
+
"""Parse de texto HCL → ``HclFile``; nunca levanta exceção."""
|
|
528
|
+
try:
|
|
529
|
+
return _Parser(text).parse()
|
|
530
|
+
except Exception as exc: # última linha de defesa — diagnóstico honesto
|
|
531
|
+
return HclFile(
|
|
532
|
+
diagnostics=(
|
|
533
|
+
HclDiagnostic(
|
|
534
|
+
code="parse-error",
|
|
535
|
+
message=f"internal parser failure: {exc!r}",
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
)
|