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,683 @@
|
|
|
1
|
+
"""Parser YAML de subconjunto delimitado — zero dependências (lf-0114).
|
|
2
|
+
|
|
3
|
+
O repositório proíbe PyYAML (deps de runtime = pilha Tree-sitter, ADR).
|
|
4
|
+
Este parser cobre apenas o subconjunto usado por specs OpenAPI/Swagger e
|
|
5
|
+
futuros artefatos CI/CD (lf-0116):
|
|
6
|
+
|
|
7
|
+
- mapeamentos por indentação (``key: value`` / ``key:`` + bloco aninhado);
|
|
8
|
+
- sequências (``- item``), incluindo itens-mapa inline (``- k: v``);
|
|
9
|
+
- escalares: aspas simples/duplas, inteiros, floats, ``true``/``false``,
|
|
10
|
+
``null``/``~``;
|
|
11
|
+
- flow single-line ``[a, b]`` / ``{k: v}`` (profundidade limitada);
|
|
12
|
+
- escalares de bloco ``|``/``>`` com chomping ``-``/``+``;
|
|
13
|
+
- comentários ``#`` e marcadores de documento ``---``/``...`` (primeiro
|
|
14
|
+
documento apenas — multi-documento vira diagnóstico ``info``).
|
|
15
|
+
|
|
16
|
+
Fora do subconjunto — sempre diagnosticado, nunca silencioso:
|
|
17
|
+
|
|
18
|
+
- âncoras ``&x``, aliases ``*x``, merge keys ``<<:``, tags ``!``/``!!``;
|
|
19
|
+
- escalares plain multi-linha e flow multi-linha;
|
|
20
|
+
- indentação com TAB; sequência ao mesmo nível da chave (``key:`` +
|
|
21
|
+
``- item`` sem indentação extra) — estilo raro fora de specs;
|
|
22
|
+
- chaves complexas ``? key``.
|
|
23
|
+
|
|
24
|
+
O parser nunca levanta exceção por conteúdo ruim: problemas viram
|
|
25
|
+
``YamlIssue`` e o parse continua em melhor esforço (extração parcial
|
|
26
|
+
honesta — spec lf-0114).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
from collections.abc import Mapping
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
|
|
35
|
+
YamlScalar = str | int | float | bool | None
|
|
36
|
+
YamlValue = YamlScalar | list["YamlValue"] | dict[str, "YamlValue"]
|
|
37
|
+
|
|
38
|
+
_MAX_DEPTH = 40
|
|
39
|
+
_MAX_FLOW_LEN = 16_384
|
|
40
|
+
_BLOCK_HEADER = re.compile(r"^[|>][+-]?\d*$")
|
|
41
|
+
_INT = re.compile(r"^[-+]?\d+$")
|
|
42
|
+
_FLOAT = re.compile(r"^[-+]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][-+]?\d+)?$")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class YamlIssue:
|
|
47
|
+
"""Problema de parse YAML — diagnóstico honesto, nunca exceção."""
|
|
48
|
+
|
|
49
|
+
code: str # ex.: "yaml-anchor-unsupported"
|
|
50
|
+
severity: str # "info" | "warning" | "error"
|
|
51
|
+
message: str
|
|
52
|
+
line: int = 0 # 1-based; 0 = não localizado
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class YamlDocument:
|
|
57
|
+
"""Resultado do parse: valor + issues + mapa key-path → linha.
|
|
58
|
+
|
|
59
|
+
``key_lines`` indexa a linha (1-based) onde cada chave de mapeamento
|
|
60
|
+
foi declarada. O caminho é a tupla de chaves normalizadas (strings);
|
|
61
|
+
itens de sequência aparecem como índice string (``"0"``, ``"1"``…).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
value: YamlValue
|
|
65
|
+
issues: tuple[YamlIssue, ...]
|
|
66
|
+
key_lines: Mapping[tuple[str, ...], int]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class _Line:
|
|
71
|
+
no: int # 1-based
|
|
72
|
+
indent: int
|
|
73
|
+
content: str # comentário removido, rstripped (sem indent)
|
|
74
|
+
ignored: bool # blank/comment-only/consumida por block scalar
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _FlowError(Exception):
|
|
78
|
+
"""Falha interna de parse flow — convertida em YamlIssue."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _Parser:
|
|
82
|
+
def __init__(self, text: str) -> None:
|
|
83
|
+
self.raw = text.splitlines()
|
|
84
|
+
self.lines: list[_Line] = []
|
|
85
|
+
self.issues: list[YamlIssue] = []
|
|
86
|
+
self.key_lines: dict[tuple[str, ...], int] = {}
|
|
87
|
+
self._tab_reported = False
|
|
88
|
+
self._build_lines()
|
|
89
|
+
|
|
90
|
+
# -- infra ---------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def _issue(self, code: str, severity: str, message: str, line: int = 0) -> None:
|
|
93
|
+
self.issues.append(YamlIssue(code, severity, message, line))
|
|
94
|
+
|
|
95
|
+
def _strip_comment(self, s: str) -> str:
|
|
96
|
+
"""Corta ``#`` iniciando comentário (pos 0 ou após espaço), fora de aspas."""
|
|
97
|
+
in_s = in_d = False
|
|
98
|
+
j = 0
|
|
99
|
+
while j < len(s):
|
|
100
|
+
ch = s[j]
|
|
101
|
+
if in_d:
|
|
102
|
+
if ch == "\\":
|
|
103
|
+
j += 2 # próximo char é escapado
|
|
104
|
+
continue
|
|
105
|
+
if ch == '"':
|
|
106
|
+
in_d = False
|
|
107
|
+
elif in_s:
|
|
108
|
+
if ch == "'":
|
|
109
|
+
in_s = False
|
|
110
|
+
elif ch == '"':
|
|
111
|
+
in_d = True
|
|
112
|
+
elif ch == "'":
|
|
113
|
+
in_s = True
|
|
114
|
+
elif ch == "#" and (j == 0 or s[j - 1] in " \t"):
|
|
115
|
+
return s[:j]
|
|
116
|
+
j += 1
|
|
117
|
+
return s
|
|
118
|
+
|
|
119
|
+
def _build_lines(self) -> None:
|
|
120
|
+
for no, raw in enumerate(self.raw, 1):
|
|
121
|
+
ws = raw[: len(raw) - len(raw.lstrip())]
|
|
122
|
+
if "\t" in ws and not self._tab_reported:
|
|
123
|
+
self._issue(
|
|
124
|
+
"tab-indent", "error",
|
|
125
|
+
"TAB in leading whitespace — line ignored", no,
|
|
126
|
+
)
|
|
127
|
+
self._tab_reported = True
|
|
128
|
+
if "\t" in ws:
|
|
129
|
+
self.lines.append(_Line(no, 0, "", ignored=True))
|
|
130
|
+
continue
|
|
131
|
+
indent = len(ws)
|
|
132
|
+
content = self._strip_comment(raw[indent:]).rstrip()
|
|
133
|
+
self.lines.append(_Line(no, indent, content, ignored=not content))
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def _n(self) -> int:
|
|
137
|
+
return len(self.lines)
|
|
138
|
+
|
|
139
|
+
def _eff(self, i: int) -> int:
|
|
140
|
+
"""Próximo índice de linha efetiva (não-ignorada) >= i."""
|
|
141
|
+
while i < self._n and self.lines[i].ignored:
|
|
142
|
+
i += 1
|
|
143
|
+
return i
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _is_doc_marker(content: str) -> bool:
|
|
147
|
+
return content == "---" or content == "..." or content.startswith("--- ")
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _is_seq_item(content: str) -> bool:
|
|
151
|
+
return content == "-" or content.startswith("- ")
|
|
152
|
+
|
|
153
|
+
# -- entrada ---------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def parse(self) -> YamlDocument:
|
|
156
|
+
i = self._eff(0)
|
|
157
|
+
if i < self._n and self.lines[i].content == "---":
|
|
158
|
+
i = self._eff(i + 1)
|
|
159
|
+
if i >= self._n:
|
|
160
|
+
value: YamlValue = None
|
|
161
|
+
else:
|
|
162
|
+
value, i = self._parse_node(i, self.lines[i].indent, (), 0)
|
|
163
|
+
j = self._eff(i)
|
|
164
|
+
if j < self._n:
|
|
165
|
+
if self._is_doc_marker(self.lines[j].content):
|
|
166
|
+
self._issue(
|
|
167
|
+
"multi-document", "info",
|
|
168
|
+
"extra YAML documents ignored — first document parsed",
|
|
169
|
+
self.lines[j].no,
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
self._issue(
|
|
173
|
+
"trailing-content", "warning",
|
|
174
|
+
"unparsed trailing content", self.lines[j].no,
|
|
175
|
+
)
|
|
176
|
+
return YamlDocument(
|
|
177
|
+
value=value, issues=tuple(self.issues),
|
|
178
|
+
key_lines=dict(self.key_lines),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# -- dispatch --------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def _parse_node(
|
|
184
|
+
self, i: int, indent: int, path: tuple[str, ...], depth: int
|
|
185
|
+
) -> tuple[YamlValue, int]:
|
|
186
|
+
if depth > _MAX_DEPTH:
|
|
187
|
+
self._issue("max-depth", "error", "nesting depth exceeded", 0)
|
|
188
|
+
return None, i
|
|
189
|
+
i = self._eff(i)
|
|
190
|
+
if i >= self._n:
|
|
191
|
+
return None, i
|
|
192
|
+
li = self.lines[i]
|
|
193
|
+
if li.indent < indent or self._is_doc_marker(li.content):
|
|
194
|
+
return None, i
|
|
195
|
+
if self._is_seq_item(li.content):
|
|
196
|
+
return self._parse_seq(i, li.indent, path, depth)
|
|
197
|
+
if self._split_key(li.content) is not None:
|
|
198
|
+
return self._parse_map(i, li.indent, path, depth)
|
|
199
|
+
# escalar de linha única
|
|
200
|
+
val = self._scalar_or_tagged(li.content, li.no)
|
|
201
|
+
j = self._eff(i + 1)
|
|
202
|
+
if j < self._n and self.lines[j].indent > li.indent:
|
|
203
|
+
self._issue(
|
|
204
|
+
"multiline-plain-scalar", "warning",
|
|
205
|
+
"continuation of plain scalar ignored",
|
|
206
|
+
self.lines[j].no,
|
|
207
|
+
)
|
|
208
|
+
while j < self._n and self.lines[j].indent > li.indent:
|
|
209
|
+
self.lines[j].ignored = True
|
|
210
|
+
j = self._eff(j + 1)
|
|
211
|
+
return val, j
|
|
212
|
+
return val, i + 1
|
|
213
|
+
|
|
214
|
+
# -- mapeamento ------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _is_key_sep(content: str, j: int) -> bool:
|
|
218
|
+
return content[j] == ":" and (
|
|
219
|
+
j + 1 == len(content) or content[j + 1] in " \t"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _split_key(self, content: str) -> tuple[str, str] | None:
|
|
223
|
+
"""``key: rest`` no primeiro ``:`` seguido de espaço/EOL, depth 0."""
|
|
224
|
+
quote = ""
|
|
225
|
+
esc = False
|
|
226
|
+
depth = 0
|
|
227
|
+
for j, ch in enumerate(content):
|
|
228
|
+
if esc:
|
|
229
|
+
esc = False
|
|
230
|
+
continue
|
|
231
|
+
if quote:
|
|
232
|
+
esc, quote = _quote_step(ch, quote)
|
|
233
|
+
continue
|
|
234
|
+
if ch in "'\"":
|
|
235
|
+
quote = ch
|
|
236
|
+
elif ch in "[{":
|
|
237
|
+
depth += 1
|
|
238
|
+
elif ch in "]}":
|
|
239
|
+
depth = max(0, depth - 1)
|
|
240
|
+
elif self._is_key_sep(content, j):
|
|
241
|
+
return content[:j].strip(), content[j + 1:].strip()
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def _norm_key(self, raw: str, line: int) -> str:
|
|
245
|
+
if raw.startswith("? "):
|
|
246
|
+
self._issue(
|
|
247
|
+
"complex-key-unsupported", "warning",
|
|
248
|
+
"explicit complex key unsupported", line,
|
|
249
|
+
)
|
|
250
|
+
raw = raw[2:].strip()
|
|
251
|
+
if raw[:1] in ("&", "!"):
|
|
252
|
+
self._issue(
|
|
253
|
+
"yaml-tag-unsupported", "warning",
|
|
254
|
+
f"key tag/anchor ignored: {raw.split()[0]!r}", line,
|
|
255
|
+
)
|
|
256
|
+
parts = raw.split(None, 1)
|
|
257
|
+
raw = parts[1] if len(parts) > 1 else ""
|
|
258
|
+
if raw[:1] in ("'", '"'):
|
|
259
|
+
val, _ = self._quoted(raw, line)
|
|
260
|
+
return val if val else raw.strip("'\"")
|
|
261
|
+
return raw.strip()
|
|
262
|
+
|
|
263
|
+
def _parse_map(
|
|
264
|
+
self, i: int, indent: int, path: tuple[str, ...], depth: int
|
|
265
|
+
) -> tuple[dict[str, YamlValue], int]:
|
|
266
|
+
out: dict[str, YamlValue] = {}
|
|
267
|
+
while True:
|
|
268
|
+
i = self._eff(i)
|
|
269
|
+
if i >= self._n:
|
|
270
|
+
break
|
|
271
|
+
li = self.lines[i]
|
|
272
|
+
if self._is_doc_marker(li.content):
|
|
273
|
+
break
|
|
274
|
+
if li.indent < indent:
|
|
275
|
+
break
|
|
276
|
+
if li.indent > indent:
|
|
277
|
+
self._issue(
|
|
278
|
+
"unexpected-indent", "warning",
|
|
279
|
+
"orphan indented line skipped", li.no,
|
|
280
|
+
)
|
|
281
|
+
i += 1
|
|
282
|
+
continue
|
|
283
|
+
if self._is_seq_item(li.content):
|
|
284
|
+
break # volta ao nível da sequência
|
|
285
|
+
kv = self._split_key(li.content)
|
|
286
|
+
if kv is None:
|
|
287
|
+
self._issue(
|
|
288
|
+
"not-a-mapping", "error",
|
|
289
|
+
f"line is not a mapping entry: {li.content[:60]!r}", li.no,
|
|
290
|
+
)
|
|
291
|
+
i += 1
|
|
292
|
+
continue
|
|
293
|
+
key = self._norm_key(kv[0], li.no)
|
|
294
|
+
self.key_lines.setdefault((*path, key), li.no)
|
|
295
|
+
if key == "<<":
|
|
296
|
+
self._issue(
|
|
297
|
+
"yaml-merge-key-unsupported", "warning",
|
|
298
|
+
"merge key '<<' parsed as literal key", li.no,
|
|
299
|
+
)
|
|
300
|
+
if key in out:
|
|
301
|
+
self._issue(
|
|
302
|
+
"duplicate-key", "warning",
|
|
303
|
+
f"duplicate key {key!r} — first value kept", li.no,
|
|
304
|
+
)
|
|
305
|
+
value, i = self._value(kv[1], i, li, (*path, key), depth)
|
|
306
|
+
out.setdefault(key, value)
|
|
307
|
+
return out, i
|
|
308
|
+
|
|
309
|
+
# -- sequência ---------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def _parse_seq(
|
|
312
|
+
self, i: int, indent: int, path: tuple[str, ...], depth: int
|
|
313
|
+
) -> tuple[list[YamlValue], int]:
|
|
314
|
+
items: list[YamlValue] = []
|
|
315
|
+
idx = 0
|
|
316
|
+
while True:
|
|
317
|
+
i = self._eff(i)
|
|
318
|
+
if i >= self._n:
|
|
319
|
+
break
|
|
320
|
+
li = self.lines[i]
|
|
321
|
+
if self._is_doc_marker(li.content):
|
|
322
|
+
break
|
|
323
|
+
if li.indent < indent:
|
|
324
|
+
break
|
|
325
|
+
if li.indent > indent:
|
|
326
|
+
self._issue(
|
|
327
|
+
"unexpected-indent", "warning",
|
|
328
|
+
"orphan indented line skipped", li.no,
|
|
329
|
+
)
|
|
330
|
+
i += 1
|
|
331
|
+
continue
|
|
332
|
+
if not self._is_seq_item(li.content):
|
|
333
|
+
break
|
|
334
|
+
after = li.content[1:]
|
|
335
|
+
stripped = after.lstrip(" ")
|
|
336
|
+
item_col = li.indent + 1 + (len(after) - len(stripped))
|
|
337
|
+
key_path = (*path, str(idx))
|
|
338
|
+
if not stripped:
|
|
339
|
+
j = self._eff(i + 1)
|
|
340
|
+
if (
|
|
341
|
+
j < self._n
|
|
342
|
+
and self.lines[j].indent > li.indent
|
|
343
|
+
and not self._is_doc_marker(self.lines[j].content)
|
|
344
|
+
):
|
|
345
|
+
v, i = self._parse_node(
|
|
346
|
+
j, self.lines[j].indent, key_path, depth + 1
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
v, i = None, i + 1
|
|
350
|
+
items.append(v)
|
|
351
|
+
elif self._split_key(stripped) is not None:
|
|
352
|
+
# item-mapa inline: reescreve a linha como entrada de mapa
|
|
353
|
+
# em ``item_col`` e delega ao parser de mapa.
|
|
354
|
+
li.indent = item_col
|
|
355
|
+
li.content = stripped
|
|
356
|
+
v, i = self._parse_map(i, item_col, key_path, depth + 1)
|
|
357
|
+
items.append(v)
|
|
358
|
+
else:
|
|
359
|
+
v, i = self._value(stripped, i, li, key_path, depth)
|
|
360
|
+
items.append(v)
|
|
361
|
+
idx += 1
|
|
362
|
+
return items, i
|
|
363
|
+
|
|
364
|
+
# -- valores -----------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
def _value(
|
|
367
|
+
self, rest: str, i: int, li: _Line, path: tuple[str, ...], depth: int
|
|
368
|
+
) -> tuple[YamlValue, int]:
|
|
369
|
+
"""Valor de um par ``key: rest`` ou item ``- rest``."""
|
|
370
|
+
if not rest:
|
|
371
|
+
j = self._eff(i + 1)
|
|
372
|
+
if (
|
|
373
|
+
j < self._n
|
|
374
|
+
and self.lines[j].indent > li.indent
|
|
375
|
+
and not self._is_doc_marker(self.lines[j].content)
|
|
376
|
+
):
|
|
377
|
+
return self._parse_node(
|
|
378
|
+
j, self.lines[j].indent, path, depth + 1
|
|
379
|
+
)
|
|
380
|
+
return None, i + 1
|
|
381
|
+
if _BLOCK_HEADER.match(rest):
|
|
382
|
+
return self._block_scalar(i, li, rest)
|
|
383
|
+
if rest[0] in "[{":
|
|
384
|
+
return self._flow(rest, li.no), i + 1
|
|
385
|
+
return self._scalar_or_tagged(rest, li.no), i + 1
|
|
386
|
+
|
|
387
|
+
def _scalar_or_tagged(self, s: str, line: int) -> YamlValue:
|
|
388
|
+
while s[:1] in ("&", "!"):
|
|
389
|
+
tok, _, rem = s.partition(" ")
|
|
390
|
+
self._issue(
|
|
391
|
+
"yaml-anchor-unsupported" if s[0] == "&"
|
|
392
|
+
else "yaml-tag-unsupported",
|
|
393
|
+
"warning",
|
|
394
|
+
f"{tok!r} ignored — value kept without anchor/tag semantics",
|
|
395
|
+
line,
|
|
396
|
+
)
|
|
397
|
+
s = rem.strip()
|
|
398
|
+
if not s:
|
|
399
|
+
return None
|
|
400
|
+
if s.startswith("*"):
|
|
401
|
+
self._issue(
|
|
402
|
+
"yaml-alias-unsupported", "warning",
|
|
403
|
+
f"alias {s.split()[0]!r} unresolved — value null", line,
|
|
404
|
+
)
|
|
405
|
+
return None
|
|
406
|
+
if s[:1] in "[{":
|
|
407
|
+
return self._flow(s, line)
|
|
408
|
+
if s[:1] in ("|", ">"):
|
|
409
|
+
# header de block scalar fora de posição (ex.: após `-` inline)
|
|
410
|
+
self._issue(
|
|
411
|
+
"misplaced-block-scalar", "warning",
|
|
412
|
+
"block scalar header in unexpected position", line,
|
|
413
|
+
)
|
|
414
|
+
return None
|
|
415
|
+
if s[:1] in ("'", '"'):
|
|
416
|
+
val, end = self._quoted(s, line)
|
|
417
|
+
if s[end:].strip():
|
|
418
|
+
self._issue(
|
|
419
|
+
"trailing-junk", "warning",
|
|
420
|
+
f"trailing content after quoted scalar: {s[end:][:40]!r}",
|
|
421
|
+
line,
|
|
422
|
+
)
|
|
423
|
+
return val
|
|
424
|
+
return self._plain(s)
|
|
425
|
+
|
|
426
|
+
def _quoted(self, s: str, line: int) -> tuple[str, int]:
|
|
427
|
+
"""Escalar entre aspas; devolve (valor, índice pós-fechamento)."""
|
|
428
|
+
if s[0] == "'":
|
|
429
|
+
return self._single_quoted(s, line)
|
|
430
|
+
return self._double_quoted(s, line)
|
|
431
|
+
|
|
432
|
+
def _single_quoted(self, s: str, line: int) -> tuple[str, int]:
|
|
433
|
+
out: list[str] = []
|
|
434
|
+
j = 1
|
|
435
|
+
while j < len(s):
|
|
436
|
+
if s[j] == "'":
|
|
437
|
+
if j + 1 < len(s) and s[j + 1] == "'":
|
|
438
|
+
out.append("'")
|
|
439
|
+
j += 2
|
|
440
|
+
continue
|
|
441
|
+
return "".join(out), j + 1
|
|
442
|
+
out.append(s[j])
|
|
443
|
+
j += 1
|
|
444
|
+
self._issue(
|
|
445
|
+
"unterminated-string", "error",
|
|
446
|
+
"unterminated single-quoted scalar", line,
|
|
447
|
+
)
|
|
448
|
+
return "".join(out), len(s)
|
|
449
|
+
|
|
450
|
+
def _dq_escape(self, s: str, j: int) -> tuple[str, int]:
|
|
451
|
+
"""Escape ``\\x``/``\\u``/simples em posição j — (char, new j)."""
|
|
452
|
+
esc = {"n": "\n", "t": "\t", "r": "\r", '"': '"', "\\": "\\", "0": "\0"}
|
|
453
|
+
nxt = s[j + 1]
|
|
454
|
+
for mark, size in (("x", 4), ("u", 6)):
|
|
455
|
+
if nxt == mark and j + size - 1 < len(s):
|
|
456
|
+
try:
|
|
457
|
+
return chr(int(s[j + 2:j + size], 16)), j + size
|
|
458
|
+
except ValueError:
|
|
459
|
+
break
|
|
460
|
+
return esc.get(nxt, nxt), j + 2
|
|
461
|
+
|
|
462
|
+
def _double_quoted(self, s: str, line: int) -> tuple[str, int]:
|
|
463
|
+
out: list[str] = []
|
|
464
|
+
j = 1
|
|
465
|
+
while j < len(s):
|
|
466
|
+
ch = s[j]
|
|
467
|
+
if ch == "\\" and j + 1 < len(s):
|
|
468
|
+
piece, j = self._dq_escape(s, j)
|
|
469
|
+
out.append(piece)
|
|
470
|
+
continue
|
|
471
|
+
if ch == '"':
|
|
472
|
+
return "".join(out), j + 1
|
|
473
|
+
out.append(ch)
|
|
474
|
+
j += 1
|
|
475
|
+
self._issue(
|
|
476
|
+
"unterminated-string", "error",
|
|
477
|
+
"unterminated double-quoted scalar", line,
|
|
478
|
+
)
|
|
479
|
+
return "".join(out), len(s)
|
|
480
|
+
|
|
481
|
+
def _plain(self, s: str) -> YamlScalar:
|
|
482
|
+
v = s.strip()
|
|
483
|
+
low = v.lower()
|
|
484
|
+
if low in ("", "~", "null"):
|
|
485
|
+
return None
|
|
486
|
+
if low == "true":
|
|
487
|
+
return True
|
|
488
|
+
if low == "false":
|
|
489
|
+
return False
|
|
490
|
+
if _INT.match(v):
|
|
491
|
+
try:
|
|
492
|
+
return int(v)
|
|
493
|
+
except ValueError:
|
|
494
|
+
return v
|
|
495
|
+
if _FLOAT.match(v):
|
|
496
|
+
try:
|
|
497
|
+
return float(v)
|
|
498
|
+
except ValueError:
|
|
499
|
+
return v
|
|
500
|
+
return v
|
|
501
|
+
|
|
502
|
+
# -- block scalar -----------------------------------------------------
|
|
503
|
+
|
|
504
|
+
def _block_scalar(
|
|
505
|
+
self, i: int, li: _Line, header: str
|
|
506
|
+
) -> tuple[str, int]:
|
|
507
|
+
chunk, j = self._collect_scalar_lines(i, li.indent)
|
|
508
|
+
while chunk and chunk[-1] == "":
|
|
509
|
+
chunk.pop()
|
|
510
|
+
value = _fold_lines(chunk) if header[0] == ">" else "\n".join(chunk)
|
|
511
|
+
if "-" not in header and value:
|
|
512
|
+
value += "\n"
|
|
513
|
+
return value, j
|
|
514
|
+
|
|
515
|
+
def _collect_scalar_lines(
|
|
516
|
+
self, i: int, parent_indent: int
|
|
517
|
+
) -> tuple[list[str], int]:
|
|
518
|
+
"""Linhas do block scalar (indent > parent); marca-as ignoradas."""
|
|
519
|
+
j = i + 1
|
|
520
|
+
block_indent: int | None = None
|
|
521
|
+
chunk: list[str] = []
|
|
522
|
+
while j < self._n:
|
|
523
|
+
raw = self.raw[j]
|
|
524
|
+
if not raw.strip():
|
|
525
|
+
chunk.append("")
|
|
526
|
+
j += 1
|
|
527
|
+
continue
|
|
528
|
+
ind = len(raw) - len(raw.lstrip(" "))
|
|
529
|
+
if ind <= parent_indent:
|
|
530
|
+
break
|
|
531
|
+
if block_indent is None:
|
|
532
|
+
block_indent = ind
|
|
533
|
+
if ind < block_indent:
|
|
534
|
+
break
|
|
535
|
+
chunk.append(raw[block_indent:])
|
|
536
|
+
self.lines[j].ignored = True
|
|
537
|
+
j += 1
|
|
538
|
+
return chunk, j
|
|
539
|
+
|
|
540
|
+
# -- flow --------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
def _flow(self, s: str, line: int) -> YamlValue:
|
|
543
|
+
if len(s) > _MAX_FLOW_LEN:
|
|
544
|
+
self._issue(
|
|
545
|
+
"flow-too-large", "error",
|
|
546
|
+
f"flow value exceeds {_MAX_FLOW_LEN} chars", line,
|
|
547
|
+
)
|
|
548
|
+
return None
|
|
549
|
+
try:
|
|
550
|
+
val, pos = self._flow_at(s, 0, 0, line)
|
|
551
|
+
except _FlowError as exc:
|
|
552
|
+
self._issue("flow-parse-error", "error", str(exc), line)
|
|
553
|
+
return None
|
|
554
|
+
if s[pos:].strip():
|
|
555
|
+
self._issue(
|
|
556
|
+
"trailing-junk", "warning",
|
|
557
|
+
f"trailing content after flow value: {s[pos:][:40]!r}", line,
|
|
558
|
+
)
|
|
559
|
+
return val
|
|
560
|
+
|
|
561
|
+
def _flow_at(
|
|
562
|
+
self, s: str, i: int, depth: int, line: int
|
|
563
|
+
) -> tuple[YamlValue, int]:
|
|
564
|
+
if depth > 8:
|
|
565
|
+
raise _FlowError("flow nesting depth exceeded")
|
|
566
|
+
i = self._flow_ws(s, i)
|
|
567
|
+
if i >= len(s):
|
|
568
|
+
raise _FlowError("unterminated flow value")
|
|
569
|
+
ch = s[i]
|
|
570
|
+
if ch == "[":
|
|
571
|
+
return self._flow_seq(s, i + 1, depth, line)
|
|
572
|
+
if ch == "{":
|
|
573
|
+
return self._flow_map(s, i + 1, depth, line)
|
|
574
|
+
if ch in ("'", '"'):
|
|
575
|
+
val, used = self._quoted(s[i:], line)
|
|
576
|
+
return val, i + used
|
|
577
|
+
if ch == "*":
|
|
578
|
+
self._issue(
|
|
579
|
+
"yaml-alias-unsupported", "warning",
|
|
580
|
+
"alias in flow position — value null", line,
|
|
581
|
+
)
|
|
582
|
+
i += 1
|
|
583
|
+
while i < len(s) and s[i] not in ",]}":
|
|
584
|
+
i += 1
|
|
585
|
+
return None, i
|
|
586
|
+
# escalar plain em flow: até , ] } no mesmo nível
|
|
587
|
+
j = i
|
|
588
|
+
while j < len(s) and s[j] not in ",]}":
|
|
589
|
+
j += 1
|
|
590
|
+
return self._plain(s[i:j]), j
|
|
591
|
+
|
|
592
|
+
@staticmethod
|
|
593
|
+
def _flow_ws(s: str, i: int) -> int:
|
|
594
|
+
while i < len(s) and s[i] in " \t":
|
|
595
|
+
i += 1
|
|
596
|
+
return i
|
|
597
|
+
|
|
598
|
+
def _flow_seq(
|
|
599
|
+
self, s: str, i: int, depth: int, line: int
|
|
600
|
+
) -> tuple[list[YamlValue], int]:
|
|
601
|
+
out: list[YamlValue] = []
|
|
602
|
+
i = self._flow_ws(s, i)
|
|
603
|
+
if i < len(s) and s[i] == "]":
|
|
604
|
+
return out, i + 1
|
|
605
|
+
while True:
|
|
606
|
+
v, i = self._flow_at(s, i, depth + 1, line)
|
|
607
|
+
out.append(v)
|
|
608
|
+
i = self._flow_ws(s, i)
|
|
609
|
+
if i >= len(s):
|
|
610
|
+
raise _FlowError("unterminated flow sequence")
|
|
611
|
+
if s[i] == "]":
|
|
612
|
+
return out, i + 1
|
|
613
|
+
if s[i] != ",":
|
|
614
|
+
raise _FlowError(f"expected ',' in flow seq, got {s[i]!r}")
|
|
615
|
+
i = self._flow_ws(s, i + 1)
|
|
616
|
+
if i < len(s) and s[i] == "]":
|
|
617
|
+
return out, i + 1
|
|
618
|
+
|
|
619
|
+
def _flow_map(
|
|
620
|
+
self, s: str, i: int, depth: int, line: int
|
|
621
|
+
) -> tuple[dict[str, YamlValue], int]:
|
|
622
|
+
out: dict[str, YamlValue] = {}
|
|
623
|
+
i = self._flow_ws(s, i)
|
|
624
|
+
if i < len(s) and s[i] == "}":
|
|
625
|
+
return out, i + 1
|
|
626
|
+
while True:
|
|
627
|
+
i = self._flow_ws(s, i)
|
|
628
|
+
if i < len(s) and s[i] in ("'", '"'):
|
|
629
|
+
key_v, used = self._quoted(s[i:], line)
|
|
630
|
+
key = str(key_v)
|
|
631
|
+
i += used
|
|
632
|
+
else:
|
|
633
|
+
j = i
|
|
634
|
+
while j < len(s) and s[j] not in ":,}":
|
|
635
|
+
j += 1
|
|
636
|
+
key = s[i:j].strip()
|
|
637
|
+
i = j
|
|
638
|
+
i = self._flow_ws(s, i)
|
|
639
|
+
if i >= len(s) or s[i] != ":":
|
|
640
|
+
raise _FlowError("expected ':' in flow map")
|
|
641
|
+
v, i = self._flow_at(s, i + 1, depth + 1, line)
|
|
642
|
+
out[key] = v
|
|
643
|
+
i = self._flow_ws(s, i)
|
|
644
|
+
if i >= len(s):
|
|
645
|
+
raise _FlowError("unterminated flow map")
|
|
646
|
+
if s[i] == "}":
|
|
647
|
+
return out, i + 1
|
|
648
|
+
if s[i] != ",":
|
|
649
|
+
raise _FlowError(f"expected ',' in flow map, got {s[i]!r}")
|
|
650
|
+
i = self._flow_ws(s, i + 1)
|
|
651
|
+
if i < len(s) and s[i] == "}":
|
|
652
|
+
return out, i + 1
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _quote_step(ch: str, quote: str) -> tuple[bool, str]:
|
|
656
|
+
"""Passo de scan dentro de aspas → ``(escapou?, quote_atual)``."""
|
|
657
|
+
if ch == "\\":
|
|
658
|
+
return True, quote
|
|
659
|
+
if ch == quote:
|
|
660
|
+
return False, ""
|
|
661
|
+
return False, quote
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _fold_lines(chunk: list[str]) -> str:
|
|
665
|
+
"""Folding ``>``: linhas consecutivas viram frase, blanks separam."""
|
|
666
|
+
paras: list[str] = []
|
|
667
|
+
cur: list[str] = []
|
|
668
|
+
for line in chunk:
|
|
669
|
+
if line == "":
|
|
670
|
+
if cur:
|
|
671
|
+
paras.append(" ".join(cur))
|
|
672
|
+
cur = []
|
|
673
|
+
paras.append("")
|
|
674
|
+
else:
|
|
675
|
+
cur.append(line.strip())
|
|
676
|
+
if cur:
|
|
677
|
+
paras.append(" ".join(cur))
|
|
678
|
+
return "\n".join(paras)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def parse_yaml_subset(text: str) -> YamlDocument:
|
|
682
|
+
"""Parseia o subconjunto YAML delimitado; nunca levanta."""
|
|
683
|
+
return _Parser(text).parse()
|