modelable 1.0.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.
Potentially problematic release.
This version of modelable might be problematic. Click here for more details.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
modelable/lsp/rename.py
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from lsprotocol import types
|
|
7
|
+
|
|
8
|
+
from modelable.lsp.workspace import LspWorkspaceIndex
|
|
9
|
+
from modelable.registry.resolver import resolve_model_ref
|
|
10
|
+
|
|
11
|
+
_QUALIFIED_REF_PATTERN = re.compile(
|
|
12
|
+
r"(?P<domain>[A-Za-z_][A-Za-z0-9_]*)\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
|
|
13
|
+
)
|
|
14
|
+
_FIELD_REF_PATTERN = re.compile(r"(?P<alias>[A-Za-z_][A-Za-z0-9_]*)\.(?P<field>[A-Za-z_][A-Za-z0-9_]*)")
|
|
15
|
+
_DECL_PATTERN = re.compile(
|
|
16
|
+
r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
|
|
17
|
+
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
|
|
18
|
+
)
|
|
19
|
+
_DOMAIN_PATTERN = re.compile(r'^\s*domain\s+(?:"(?P<quoted>[^"]+)"|(?P<name>[A-Za-z_][A-Za-z0-9_]*))')
|
|
20
|
+
_MODEL_FIELD_PATTERN = re.compile(
|
|
21
|
+
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:"
|
|
22
|
+
)
|
|
23
|
+
_PROJECTION_FIELD_PATTERN = re.compile(
|
|
24
|
+
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
|
|
25
|
+
)
|
|
26
|
+
_WORD_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
27
|
+
_MODEL_DECL_KINDS = {"entity", "aggregate", "event", "value"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class _Target:
|
|
32
|
+
kind: str
|
|
33
|
+
domain: str
|
|
34
|
+
name: str
|
|
35
|
+
version: int
|
|
36
|
+
field_name: str | None
|
|
37
|
+
range: types.Range
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_prepare_rename(
|
|
41
|
+
index: LspWorkspaceIndex,
|
|
42
|
+
uri: str,
|
|
43
|
+
line: int,
|
|
44
|
+
character: int,
|
|
45
|
+
) -> types.Range | None:
|
|
46
|
+
target = _target_at(index, uri, line, character)
|
|
47
|
+
if target is None:
|
|
48
|
+
return None
|
|
49
|
+
return target.range
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_rename(
|
|
53
|
+
index: LspWorkspaceIndex,
|
|
54
|
+
uri: str,
|
|
55
|
+
line: int,
|
|
56
|
+
character: int,
|
|
57
|
+
new_name: str,
|
|
58
|
+
) -> types.WorkspaceEdit | None:
|
|
59
|
+
source = index.documents.get(uri)
|
|
60
|
+
workspace = index.workspace
|
|
61
|
+
if source is None or workspace is None:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
target = _target_at(index, uri, line, character)
|
|
65
|
+
if target is None:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
changes: dict[str, list[types.TextEdit]] = {}
|
|
69
|
+
|
|
70
|
+
if target.kind in {"model_decl", "projection_decl"}:
|
|
71
|
+
_add_decl_renames(changes, workspace, target, new_name)
|
|
72
|
+
elif target.kind == "model_field":
|
|
73
|
+
_add_model_field_renames(changes, workspace, target, new_name)
|
|
74
|
+
elif target.kind == "projection_field":
|
|
75
|
+
_add_projection_field_renames(changes, workspace, target, new_name)
|
|
76
|
+
else:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
if not changes:
|
|
80
|
+
return None
|
|
81
|
+
return types.WorkspaceEdit(changes=changes)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _target_at(index: LspWorkspaceIndex, uri: str, line: int, character: int) -> _Target | None:
|
|
85
|
+
source = index.documents.get(uri)
|
|
86
|
+
workspace = index.workspace
|
|
87
|
+
if source is None or workspace is None:
|
|
88
|
+
return None
|
|
89
|
+
lines = source.text.splitlines()
|
|
90
|
+
if line < 0 or line >= len(lines):
|
|
91
|
+
return None
|
|
92
|
+
text_line = lines[line]
|
|
93
|
+
|
|
94
|
+
for match in _QUALIFIED_REF_PATTERN.finditer(text_line):
|
|
95
|
+
if not _contains(match.start(), match.end(), character):
|
|
96
|
+
continue
|
|
97
|
+
domain = match.group("domain")
|
|
98
|
+
name = match.group("name")
|
|
99
|
+
version = int(match.group("version"))
|
|
100
|
+
domain_def = next((d for d in workspace.mdl.domains if d.name == domain), None)
|
|
101
|
+
if domain_def is None:
|
|
102
|
+
return None
|
|
103
|
+
if name in domain_def.models:
|
|
104
|
+
return _Target(
|
|
105
|
+
kind="model_decl",
|
|
106
|
+
domain=domain,
|
|
107
|
+
name=name,
|
|
108
|
+
version=version,
|
|
109
|
+
field_name=None,
|
|
110
|
+
range=types.Range(
|
|
111
|
+
start=types.Position(line=line, character=match.start("name")),
|
|
112
|
+
end=types.Position(line=line, character=match.end("name")),
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
if name in domain_def.projections:
|
|
116
|
+
return _Target(
|
|
117
|
+
kind="projection_decl",
|
|
118
|
+
domain=domain,
|
|
119
|
+
name=name,
|
|
120
|
+
version=version,
|
|
121
|
+
field_name=None,
|
|
122
|
+
range=types.Range(
|
|
123
|
+
start=types.Position(line=line, character=match.start("name")),
|
|
124
|
+
end=types.Position(line=line, character=match.end("name")),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
for match in _FIELD_REF_PATTERN.finditer(text_line):
|
|
130
|
+
if not _contains(match.start(), match.end(), character):
|
|
131
|
+
continue
|
|
132
|
+
scope = _current_scope(source.text, line)
|
|
133
|
+
if scope is None:
|
|
134
|
+
return None
|
|
135
|
+
domain_name, kind, name, version = scope
|
|
136
|
+
if kind == "model":
|
|
137
|
+
return _Target(
|
|
138
|
+
kind="model_field",
|
|
139
|
+
domain=domain_name,
|
|
140
|
+
name=name,
|
|
141
|
+
version=version,
|
|
142
|
+
field_name=match.group("field"),
|
|
143
|
+
range=types.Range(
|
|
144
|
+
start=types.Position(line=line, character=match.start("field")),
|
|
145
|
+
end=types.Position(line=line, character=match.end("field")),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
alias_map = _projection_aliases(workspace, domain_name, name, version)
|
|
149
|
+
target_model = alias_map.get(match.group("alias"))
|
|
150
|
+
if target_model is not None and match.group("field"):
|
|
151
|
+
return _Target(
|
|
152
|
+
kind="model_field",
|
|
153
|
+
domain=target_model[0],
|
|
154
|
+
name=target_model[1],
|
|
155
|
+
version=target_model[2],
|
|
156
|
+
field_name=match.group("field"),
|
|
157
|
+
range=types.Range(
|
|
158
|
+
start=types.Position(line=line, character=match.start("field")),
|
|
159
|
+
end=types.Position(line=line, character=match.end("field")),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
return _Target(
|
|
163
|
+
kind="projection_field",
|
|
164
|
+
domain=domain_name,
|
|
165
|
+
name=name,
|
|
166
|
+
version=version,
|
|
167
|
+
field_name=match.group("field"),
|
|
168
|
+
range=types.Range(
|
|
169
|
+
start=types.Position(line=line, character=match.start("field")),
|
|
170
|
+
end=types.Position(line=line, character=match.end("field")),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
word = _word_at(text_line, character)
|
|
175
|
+
if word is None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
scope = _current_scope(source.text, line)
|
|
179
|
+
if scope is None:
|
|
180
|
+
return None
|
|
181
|
+
domain_name, kind, name, version = scope
|
|
182
|
+
|
|
183
|
+
if word == name:
|
|
184
|
+
return _Target(
|
|
185
|
+
kind="model_decl" if kind == "model" else "projection_decl",
|
|
186
|
+
domain=domain_name,
|
|
187
|
+
name=name,
|
|
188
|
+
version=version,
|
|
189
|
+
field_name=None,
|
|
190
|
+
range=types.Range(
|
|
191
|
+
start=types.Position(line=line, character=_word_start(text_line, word)),
|
|
192
|
+
end=types.Position(line=line, character=_word_end(text_line, word)),
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if kind == "model":
|
|
197
|
+
if _is_field_name(workspace, domain_name, name, version, word):
|
|
198
|
+
return _Target(
|
|
199
|
+
kind="model_field",
|
|
200
|
+
domain=domain_name,
|
|
201
|
+
name=name,
|
|
202
|
+
version=version,
|
|
203
|
+
field_name=word,
|
|
204
|
+
range=types.Range(
|
|
205
|
+
start=types.Position(line=line, character=_word_start(text_line, word)),
|
|
206
|
+
end=types.Position(line=line, character=_word_end(text_line, word)),
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
if _is_projection_field_name(workspace, domain_name, name, version, word):
|
|
211
|
+
return _Target(
|
|
212
|
+
kind="projection_field",
|
|
213
|
+
domain=domain_name,
|
|
214
|
+
name=name,
|
|
215
|
+
version=version,
|
|
216
|
+
field_name=word,
|
|
217
|
+
range=types.Range(
|
|
218
|
+
start=types.Position(line=line, character=_word_start(text_line, word)),
|
|
219
|
+
end=types.Position(line=line, character=_word_end(text_line, word)),
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _add_decl_renames(
|
|
226
|
+
changes: dict[str, list[types.TextEdit]],
|
|
227
|
+
workspace,
|
|
228
|
+
target: _Target,
|
|
229
|
+
new_name: str,
|
|
230
|
+
) -> None:
|
|
231
|
+
decl_kind = "model" if target.kind == "model_decl" else "projection"
|
|
232
|
+
declaration = _find_decl_location(workspace, target.domain, decl_kind, target.name, target.version)
|
|
233
|
+
if declaration is not None:
|
|
234
|
+
changes.setdefault(declaration.uri, []).append(types.TextEdit(range=declaration.range, new_text=new_name))
|
|
235
|
+
|
|
236
|
+
old_ref = f"{target.domain}.{target.name}@{target.version}"
|
|
237
|
+
for source in workspace.sources:
|
|
238
|
+
edits = changes.setdefault(source.uri, [])
|
|
239
|
+
lines = source.text.splitlines()
|
|
240
|
+
for line_no, line in enumerate(lines):
|
|
241
|
+
for match in _QUALIFIED_REF_PATTERN.finditer(line):
|
|
242
|
+
ref = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
|
|
243
|
+
if ref != old_ref:
|
|
244
|
+
continue
|
|
245
|
+
edits.append(
|
|
246
|
+
types.TextEdit(
|
|
247
|
+
range=types.Range(
|
|
248
|
+
start=types.Position(line=line_no, character=match.start("name")),
|
|
249
|
+
end=types.Position(line=line_no, character=match.end("name")),
|
|
250
|
+
),
|
|
251
|
+
new_text=new_name,
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _add_model_field_renames(
|
|
257
|
+
changes: dict[str, list[types.TextEdit]],
|
|
258
|
+
workspace,
|
|
259
|
+
target: _Target,
|
|
260
|
+
new_name: str,
|
|
261
|
+
) -> None:
|
|
262
|
+
declaration = _find_source_field_location(
|
|
263
|
+
workspace,
|
|
264
|
+
target.domain,
|
|
265
|
+
target.name,
|
|
266
|
+
target.version,
|
|
267
|
+
target.field_name or "",
|
|
268
|
+
)
|
|
269
|
+
if declaration is not None:
|
|
270
|
+
changes.setdefault(declaration.uri, []).append(types.TextEdit(range=declaration.range, new_text=new_name))
|
|
271
|
+
|
|
272
|
+
for source in workspace.sources:
|
|
273
|
+
lines = source.text.splitlines()
|
|
274
|
+
alias_map: dict[str, tuple[str, str, int]] = {}
|
|
275
|
+
current_domain: str | None = None
|
|
276
|
+
current_projection: tuple[str, int] | None = None
|
|
277
|
+
for line_no, line in enumerate(lines):
|
|
278
|
+
domain_match = _DOMAIN_PATTERN.match(line)
|
|
279
|
+
if domain_match:
|
|
280
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
281
|
+
current_projection = None
|
|
282
|
+
alias_map = {}
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
decl_match = _DECL_PATTERN.match(line)
|
|
286
|
+
if decl_match and current_domain is not None:
|
|
287
|
+
if decl_match.group("kind") == "projection":
|
|
288
|
+
current_projection = (decl_match.group("name"), int(decl_match.group("version")))
|
|
289
|
+
alias_map = _projection_aliases(workspace, current_domain, *current_projection)
|
|
290
|
+
else:
|
|
291
|
+
current_projection = None
|
|
292
|
+
alias_map = {}
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
if current_projection is None:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
for match in _FIELD_REF_PATTERN.finditer(line):
|
|
299
|
+
target_model = alias_map.get(match.group("alias"))
|
|
300
|
+
if target_model is None:
|
|
301
|
+
continue
|
|
302
|
+
if target_model != (target.domain, target.name, target.version):
|
|
303
|
+
continue
|
|
304
|
+
if match.group("field") != target.field_name:
|
|
305
|
+
continue
|
|
306
|
+
changes.setdefault(source.uri, []).append(
|
|
307
|
+
types.TextEdit(
|
|
308
|
+
range=types.Range(
|
|
309
|
+
start=types.Position(line=line_no, character=match.start("field")),
|
|
310
|
+
end=types.Position(line=line_no, character=match.end("field")),
|
|
311
|
+
),
|
|
312
|
+
new_text=new_name,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _add_projection_field_renames(
|
|
318
|
+
changes: dict[str, list[types.TextEdit]],
|
|
319
|
+
workspace,
|
|
320
|
+
target: _Target,
|
|
321
|
+
new_name: str,
|
|
322
|
+
) -> None:
|
|
323
|
+
declaration = _find_source_field_location(
|
|
324
|
+
workspace,
|
|
325
|
+
target.domain,
|
|
326
|
+
target.name,
|
|
327
|
+
target.version,
|
|
328
|
+
target.field_name or "",
|
|
329
|
+
)
|
|
330
|
+
if declaration is not None:
|
|
331
|
+
changes.setdefault(declaration.uri, []).append(types.TextEdit(range=declaration.range, new_text=new_name))
|
|
332
|
+
|
|
333
|
+
for source in workspace.sources:
|
|
334
|
+
lines = source.text.splitlines()
|
|
335
|
+
alias_map: dict[str, tuple[str, str, int]] = {}
|
|
336
|
+
current_domain: str | None = None
|
|
337
|
+
current_projection: tuple[str, int] | None = None
|
|
338
|
+
for line_no, line in enumerate(lines):
|
|
339
|
+
domain_match = _DOMAIN_PATTERN.match(line)
|
|
340
|
+
if domain_match:
|
|
341
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
342
|
+
current_projection = None
|
|
343
|
+
alias_map = {}
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
decl_match = _DECL_PATTERN.match(line)
|
|
347
|
+
if decl_match and current_domain is not None:
|
|
348
|
+
if decl_match.group("kind") == "projection":
|
|
349
|
+
current_projection = (decl_match.group("name"), int(decl_match.group("version")))
|
|
350
|
+
alias_map = _projection_aliases(workspace, current_domain, *current_projection)
|
|
351
|
+
else:
|
|
352
|
+
current_projection = None
|
|
353
|
+
alias_map = {}
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
if current_projection is None:
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
for match in _FIELD_REF_PATTERN.finditer(line):
|
|
360
|
+
target_model = alias_map.get(match.group("alias"))
|
|
361
|
+
if target_model is None:
|
|
362
|
+
continue
|
|
363
|
+
if target_model != (target.domain, target.name, target.version):
|
|
364
|
+
continue
|
|
365
|
+
if match.group("field") != target.field_name:
|
|
366
|
+
continue
|
|
367
|
+
changes.setdefault(source.uri, []).append(
|
|
368
|
+
types.TextEdit(
|
|
369
|
+
range=types.Range(
|
|
370
|
+
start=types.Position(line=line_no, character=match.start("field")),
|
|
371
|
+
end=types.Position(line=line_no, character=match.end("field")),
|
|
372
|
+
),
|
|
373
|
+
new_text=new_name,
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _projection_aliases(
|
|
379
|
+
workspace,
|
|
380
|
+
domain_name: str,
|
|
381
|
+
projection_name: str,
|
|
382
|
+
version: int,
|
|
383
|
+
) -> dict[str, tuple[str, str, int]]:
|
|
384
|
+
domain = next((item for item in workspace.mdl.domains if item.name == domain_name), None)
|
|
385
|
+
if domain is None:
|
|
386
|
+
return {}
|
|
387
|
+
versions = domain.projections.get(projection_name, [])
|
|
388
|
+
projection_version = next((item for item in versions if item.version == version), None)
|
|
389
|
+
if projection_version is None:
|
|
390
|
+
return {}
|
|
391
|
+
|
|
392
|
+
aliases: dict[str, tuple[str, str, int]] = {}
|
|
393
|
+
for source_ref in [projection_version.source, *projection_version.joins]:
|
|
394
|
+
try:
|
|
395
|
+
resolved = resolve_model_ref(workspace.mdl, source_ref.model, source_ref.version)
|
|
396
|
+
except LookupError:
|
|
397
|
+
continue
|
|
398
|
+
aliases[source_ref.alias] = (
|
|
399
|
+
resolved.domain_name,
|
|
400
|
+
resolved.model_name,
|
|
401
|
+
resolved.version.version,
|
|
402
|
+
)
|
|
403
|
+
return aliases
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _find_source_field_location(
|
|
407
|
+
workspace,
|
|
408
|
+
domain_name: str,
|
|
409
|
+
model_name: str,
|
|
410
|
+
version: int,
|
|
411
|
+
field_name: str,
|
|
412
|
+
) -> types.Location | None:
|
|
413
|
+
for kind in ("model", "projection"):
|
|
414
|
+
location = _find_field_location(workspace, domain_name, kind, model_name, version, field_name)
|
|
415
|
+
if location is not None:
|
|
416
|
+
return location
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _find_field_location(
|
|
421
|
+
workspace,
|
|
422
|
+
domain_name: str,
|
|
423
|
+
kind: str,
|
|
424
|
+
name: str,
|
|
425
|
+
version: int,
|
|
426
|
+
field_name: str,
|
|
427
|
+
) -> types.Location | None:
|
|
428
|
+
for source in workspace.sources:
|
|
429
|
+
current_domain: str | None = None
|
|
430
|
+
active = False
|
|
431
|
+
lines = source.text.splitlines()
|
|
432
|
+
pattern = _MODEL_FIELD_PATTERN if kind == "model" else _PROJECTION_FIELD_PATTERN
|
|
433
|
+
for line_no, line in enumerate(lines):
|
|
434
|
+
domain_match = _DOMAIN_PATTERN.match(line)
|
|
435
|
+
if domain_match:
|
|
436
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
437
|
+
active = False
|
|
438
|
+
continue
|
|
439
|
+
decl_match = _DECL_PATTERN.match(line)
|
|
440
|
+
if decl_match and current_domain == domain_name:
|
|
441
|
+
decl_kind = decl_match.group("kind")
|
|
442
|
+
active = (
|
|
443
|
+
(decl_kind in _MODEL_DECL_KINDS if kind == "model" else decl_kind == kind)
|
|
444
|
+
and decl_match.group("name") == name
|
|
445
|
+
and int(decl_match.group("version")) == version
|
|
446
|
+
)
|
|
447
|
+
continue
|
|
448
|
+
if not active:
|
|
449
|
+
continue
|
|
450
|
+
field_match = pattern.match(line)
|
|
451
|
+
if field_match and field_match.group("name") == field_name:
|
|
452
|
+
return types.Location(
|
|
453
|
+
uri=source.uri,
|
|
454
|
+
range=types.Range(
|
|
455
|
+
start=types.Position(line=line_no, character=field_match.start("name")),
|
|
456
|
+
end=types.Position(line=line_no, character=field_match.end("name")),
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
if line.strip() == "}":
|
|
460
|
+
active = False
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _find_decl_location(
|
|
465
|
+
workspace,
|
|
466
|
+
domain_name: str,
|
|
467
|
+
kind: str,
|
|
468
|
+
name: str,
|
|
469
|
+
version: int,
|
|
470
|
+
) -> types.Location | None:
|
|
471
|
+
for source in workspace.sources:
|
|
472
|
+
current_domain: str | None = None
|
|
473
|
+
lines = source.text.splitlines()
|
|
474
|
+
for line_no, line in enumerate(lines):
|
|
475
|
+
domain_match = _DOMAIN_PATTERN.match(line)
|
|
476
|
+
if domain_match:
|
|
477
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
478
|
+
continue
|
|
479
|
+
decl_match = _DECL_PATTERN.match(line)
|
|
480
|
+
if not decl_match or current_domain != domain_name:
|
|
481
|
+
continue
|
|
482
|
+
decl_kind = decl_match.group("kind")
|
|
483
|
+
if kind == "model":
|
|
484
|
+
if decl_kind not in _MODEL_DECL_KINDS:
|
|
485
|
+
continue
|
|
486
|
+
elif decl_kind != kind:
|
|
487
|
+
continue
|
|
488
|
+
if decl_match.group("name") != name:
|
|
489
|
+
continue
|
|
490
|
+
if int(decl_match.group("version")) != version:
|
|
491
|
+
continue
|
|
492
|
+
return types.Location(
|
|
493
|
+
uri=source.uri,
|
|
494
|
+
range=types.Range(
|
|
495
|
+
start=types.Position(line=line_no, character=decl_match.start("name")),
|
|
496
|
+
end=types.Position(line=line_no, character=decl_match.end("name")),
|
|
497
|
+
),
|
|
498
|
+
)
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _current_scope(text: str, line: int) -> tuple[str, str, str, int] | None:
|
|
503
|
+
lines = text.splitlines()
|
|
504
|
+
current_domain: str | None = None
|
|
505
|
+
current_kind: str | None = None
|
|
506
|
+
current_name: str | None = None
|
|
507
|
+
current_version: int | None = None
|
|
508
|
+
for item in lines[: line + 1]:
|
|
509
|
+
domain_match = _DOMAIN_PATTERN.match(item)
|
|
510
|
+
if domain_match:
|
|
511
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
512
|
+
current_kind = None
|
|
513
|
+
current_name = None
|
|
514
|
+
current_version = None
|
|
515
|
+
continue
|
|
516
|
+
decl_match = _DECL_PATTERN.match(item)
|
|
517
|
+
if decl_match and current_domain is not None:
|
|
518
|
+
current_kind = "model" if decl_match.group("kind") != "projection" else "projection"
|
|
519
|
+
current_name = decl_match.group("name")
|
|
520
|
+
current_version = int(decl_match.group("version"))
|
|
521
|
+
if current_domain and current_kind and current_name and current_version is not None:
|
|
522
|
+
return current_domain, current_kind, current_name, current_version
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _word_at(text_line: str, character: int) -> str | None:
|
|
527
|
+
for match in _WORD_PATTERN.finditer(text_line):
|
|
528
|
+
if _contains(match.start(), match.end(), character):
|
|
529
|
+
return match.group(0)
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _word_start(text_line: str, word: str) -> int:
|
|
534
|
+
match = re.search(rf"\b{re.escape(word)}\b", text_line)
|
|
535
|
+
return match.start() if match is not None else 0
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _word_end(text_line: str, word: str) -> int:
|
|
539
|
+
match = re.search(rf"\b{re.escape(word)}\b", text_line)
|
|
540
|
+
return match.end() if match is not None else len(text_line)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _is_field_name(
|
|
544
|
+
workspace,
|
|
545
|
+
domain_name: str,
|
|
546
|
+
model_name: str,
|
|
547
|
+
version: int,
|
|
548
|
+
field_name: str,
|
|
549
|
+
) -> bool:
|
|
550
|
+
return _find_field_location(workspace, domain_name, "model", model_name, version, field_name) is not None
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _is_projection_field_name(
|
|
554
|
+
workspace,
|
|
555
|
+
domain_name: str,
|
|
556
|
+
projection_name: str,
|
|
557
|
+
version: int,
|
|
558
|
+
field_name: str,
|
|
559
|
+
) -> bool:
|
|
560
|
+
return _find_field_location(workspace, domain_name, "projection", projection_name, version, field_name) is not None
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _contains(start: int, end: int, character: int) -> bool:
|
|
564
|
+
return start <= character <= max(end - 1, start)
|