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
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from lsprotocol import types
|
|
6
|
+
|
|
7
|
+
from modelable.llm.context import parse_model_ref
|
|
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
|
+
_WORD_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
21
|
+
_MODEL_FIELD_PATTERN = re.compile(
|
|
22
|
+
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:"
|
|
23
|
+
)
|
|
24
|
+
_PROJECTION_FIELD_PATTERN = re.compile(
|
|
25
|
+
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
|
|
26
|
+
)
|
|
27
|
+
_MODEL_DECL_KINDS = {"entity", "aggregate", "event", "value"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_references(
|
|
31
|
+
index: LspWorkspaceIndex,
|
|
32
|
+
uri: str,
|
|
33
|
+
line: int,
|
|
34
|
+
character: int,
|
|
35
|
+
include_declaration: bool,
|
|
36
|
+
) -> list[types.Location] | None:
|
|
37
|
+
source = index.documents.get(uri)
|
|
38
|
+
workspace = index.workspace
|
|
39
|
+
if source is None or workspace is None:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
lines = source.text.splitlines()
|
|
43
|
+
if line < 0 or line >= len(lines):
|
|
44
|
+
return None
|
|
45
|
+
text_line = lines[line]
|
|
46
|
+
|
|
47
|
+
for match in _QUALIFIED_REF_PATTERN.finditer(text_line):
|
|
48
|
+
if _contains(match.start(), match.end(), character):
|
|
49
|
+
ref = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
|
|
50
|
+
return _references_for_qualified_ref(workspace, ref, include_declaration)
|
|
51
|
+
|
|
52
|
+
for match in _FIELD_REF_PATTERN.finditer(text_line):
|
|
53
|
+
if _contains(match.start(), match.end(), character):
|
|
54
|
+
return _references_for_field_reference(
|
|
55
|
+
workspace,
|
|
56
|
+
source.text,
|
|
57
|
+
line,
|
|
58
|
+
match.group("alias"),
|
|
59
|
+
match.group("field"),
|
|
60
|
+
include_declaration,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
word = _word_at(text_line, character)
|
|
64
|
+
if word is None:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
scope = _current_scope(source.text, line)
|
|
68
|
+
if scope is None:
|
|
69
|
+
return None
|
|
70
|
+
domain_name, kind, name, version = scope
|
|
71
|
+
|
|
72
|
+
if word == name:
|
|
73
|
+
return _references_for_decl(workspace, domain_name, kind, name, version, include_declaration)
|
|
74
|
+
|
|
75
|
+
if kind == "model":
|
|
76
|
+
return _references_for_source_field(
|
|
77
|
+
workspace,
|
|
78
|
+
domain_name,
|
|
79
|
+
name,
|
|
80
|
+
version,
|
|
81
|
+
word,
|
|
82
|
+
include_declaration,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return _references_for_projection_field(
|
|
86
|
+
workspace,
|
|
87
|
+
domain_name,
|
|
88
|
+
name,
|
|
89
|
+
version,
|
|
90
|
+
word,
|
|
91
|
+
include_declaration,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _references_for_qualified_ref(
|
|
96
|
+
workspace,
|
|
97
|
+
ref: str,
|
|
98
|
+
include_declaration: bool,
|
|
99
|
+
) -> list[types.Location]:
|
|
100
|
+
model_ref = parse_model_ref(ref)
|
|
101
|
+
domain = next((d for d in workspace.mdl.domains if d.name == model_ref.domain), None)
|
|
102
|
+
if domain is None:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
if model_ref.name in domain.models:
|
|
106
|
+
kind = "model"
|
|
107
|
+
elif model_ref.name in domain.projections:
|
|
108
|
+
kind = "projection"
|
|
109
|
+
else:
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
locations = _reference_locations_for_decl(workspace, model_ref.domain, kind, model_ref.name, model_ref.version)
|
|
113
|
+
if include_declaration:
|
|
114
|
+
decl = _find_decl_location(workspace, model_ref.domain, kind, model_ref.name, model_ref.version)
|
|
115
|
+
if decl is not None:
|
|
116
|
+
locations = [decl, *locations]
|
|
117
|
+
return _dedupe_locations(locations)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _references_for_field_reference(
|
|
121
|
+
workspace,
|
|
122
|
+
text: str,
|
|
123
|
+
line: int,
|
|
124
|
+
alias: str,
|
|
125
|
+
field_name: str,
|
|
126
|
+
include_declaration: bool,
|
|
127
|
+
) -> list[types.Location]:
|
|
128
|
+
scope = _current_scope(text, line)
|
|
129
|
+
if scope is None:
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
domain_name, kind, name, version = scope
|
|
133
|
+
if kind == "model":
|
|
134
|
+
return _references_for_source_field(
|
|
135
|
+
workspace,
|
|
136
|
+
domain_name,
|
|
137
|
+
name,
|
|
138
|
+
version,
|
|
139
|
+
field_name,
|
|
140
|
+
include_declaration,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
domain = next((d for d in workspace.mdl.domains if d.name == domain_name), None)
|
|
144
|
+
if domain is None:
|
|
145
|
+
return []
|
|
146
|
+
versions = domain.projections.get(name, [])
|
|
147
|
+
projection_version = next((item for item in versions if item.version == version), None)
|
|
148
|
+
if projection_version is None:
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
for source_ref in [projection_version.source, *projection_version.joins]:
|
|
152
|
+
if source_ref.alias != alias:
|
|
153
|
+
continue
|
|
154
|
+
try:
|
|
155
|
+
resolved = resolve_model_ref(
|
|
156
|
+
workspace.mdl,
|
|
157
|
+
source_ref.model,
|
|
158
|
+
source_ref.version,
|
|
159
|
+
)
|
|
160
|
+
except LookupError:
|
|
161
|
+
continue
|
|
162
|
+
return _references_for_source_field(
|
|
163
|
+
workspace,
|
|
164
|
+
resolved.domain_name,
|
|
165
|
+
resolved.model_name,
|
|
166
|
+
resolved.version.version,
|
|
167
|
+
field_name,
|
|
168
|
+
include_declaration,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if include_declaration:
|
|
172
|
+
location = _find_field_location(
|
|
173
|
+
workspace,
|
|
174
|
+
domain_name,
|
|
175
|
+
"projection",
|
|
176
|
+
name,
|
|
177
|
+
version,
|
|
178
|
+
field_name,
|
|
179
|
+
)
|
|
180
|
+
return [location] if location is not None else []
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _references_for_decl(
|
|
185
|
+
workspace,
|
|
186
|
+
domain_name: str,
|
|
187
|
+
kind: str,
|
|
188
|
+
name: str,
|
|
189
|
+
version: int,
|
|
190
|
+
include_declaration: bool,
|
|
191
|
+
) -> list[types.Location]:
|
|
192
|
+
locations = _reference_locations_for_decl(workspace, domain_name, kind, name, version)
|
|
193
|
+
if include_declaration:
|
|
194
|
+
decl = _find_decl_location(workspace, domain_name, kind, name, version)
|
|
195
|
+
if decl is not None:
|
|
196
|
+
locations = [decl, *locations]
|
|
197
|
+
return _dedupe_locations(locations)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _references_for_source_field(
|
|
201
|
+
workspace,
|
|
202
|
+
domain_name: str,
|
|
203
|
+
model_name: str,
|
|
204
|
+
version: int,
|
|
205
|
+
field_name: str,
|
|
206
|
+
include_declaration: bool,
|
|
207
|
+
) -> list[types.Location]:
|
|
208
|
+
locations: list[types.Location] = []
|
|
209
|
+
if include_declaration:
|
|
210
|
+
decl = _find_source_field_location(workspace, domain_name, model_name, version, field_name)
|
|
211
|
+
if decl is not None:
|
|
212
|
+
locations.append(decl)
|
|
213
|
+
|
|
214
|
+
for source in workspace.sources:
|
|
215
|
+
current_domain: str | None = None
|
|
216
|
+
current_projection: tuple[str, int] | None = None
|
|
217
|
+
alias_map: dict[str, tuple[str, str, int]] = {}
|
|
218
|
+
lines = source.text.splitlines()
|
|
219
|
+
|
|
220
|
+
for line_no, line_text in enumerate(lines):
|
|
221
|
+
domain_match = _DOMAIN_PATTERN.match(line_text)
|
|
222
|
+
if domain_match:
|
|
223
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
224
|
+
current_projection = None
|
|
225
|
+
alias_map = {}
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
decl_match = _DECL_PATTERN.match(line_text)
|
|
229
|
+
if decl_match and current_domain is not None:
|
|
230
|
+
if decl_match.group("kind") == "projection":
|
|
231
|
+
current_projection = (decl_match.group("name"), int(decl_match.group("version")))
|
|
232
|
+
alias_map = _projection_aliases(workspace, current_domain, *current_projection)
|
|
233
|
+
else:
|
|
234
|
+
current_projection = None
|
|
235
|
+
alias_map = {}
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if current_projection is None:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
for match in _FIELD_REF_PATTERN.finditer(line_text):
|
|
242
|
+
target = alias_map.get(match.group("alias"))
|
|
243
|
+
if target is None:
|
|
244
|
+
continue
|
|
245
|
+
if target != (domain_name, model_name, version):
|
|
246
|
+
continue
|
|
247
|
+
if match.group("field") != field_name:
|
|
248
|
+
continue
|
|
249
|
+
if not _field_exists(workspace, *target, field_name):
|
|
250
|
+
continue
|
|
251
|
+
locations.append(
|
|
252
|
+
types.Location(
|
|
253
|
+
uri=source.uri,
|
|
254
|
+
range=types.Range(
|
|
255
|
+
start=types.Position(line=line_no, character=match.start()),
|
|
256
|
+
end=types.Position(line=line_no, character=match.end()),
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return _dedupe_locations(locations)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _references_for_projection_field(
|
|
265
|
+
workspace,
|
|
266
|
+
domain_name: str,
|
|
267
|
+
projection_name: str,
|
|
268
|
+
version: int,
|
|
269
|
+
field_name: str,
|
|
270
|
+
include_declaration: bool,
|
|
271
|
+
) -> list[types.Location]:
|
|
272
|
+
return _references_for_source_field(
|
|
273
|
+
workspace,
|
|
274
|
+
domain_name,
|
|
275
|
+
projection_name,
|
|
276
|
+
version,
|
|
277
|
+
field_name,
|
|
278
|
+
include_declaration,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _reference_locations_for_decl(
|
|
283
|
+
workspace,
|
|
284
|
+
domain_name: str,
|
|
285
|
+
kind: str,
|
|
286
|
+
name: str,
|
|
287
|
+
version: int,
|
|
288
|
+
) -> list[types.Location]:
|
|
289
|
+
ref = f"{domain_name}.{name}@{version}"
|
|
290
|
+
locations: list[types.Location] = []
|
|
291
|
+
for source in workspace.sources:
|
|
292
|
+
lines = source.text.splitlines()
|
|
293
|
+
for line_no, line_text in enumerate(lines):
|
|
294
|
+
for match in _QUALIFIED_REF_PATTERN.finditer(line_text):
|
|
295
|
+
candidate = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
|
|
296
|
+
if candidate != ref:
|
|
297
|
+
continue
|
|
298
|
+
locations.append(
|
|
299
|
+
types.Location(
|
|
300
|
+
uri=source.uri,
|
|
301
|
+
range=types.Range(
|
|
302
|
+
start=types.Position(line=line_no, character=match.start()),
|
|
303
|
+
end=types.Position(line=line_no, character=match.end()),
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
return locations
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _projection_aliases(
|
|
311
|
+
workspace,
|
|
312
|
+
domain_name: str,
|
|
313
|
+
projection_name: str,
|
|
314
|
+
version: int,
|
|
315
|
+
) -> dict[str, tuple[str, str, int]]:
|
|
316
|
+
domain = next((item for item in workspace.mdl.domains if item.name == domain_name), None)
|
|
317
|
+
if domain is None:
|
|
318
|
+
return {}
|
|
319
|
+
versions = domain.projections.get(projection_name, [])
|
|
320
|
+
projection_version = next((item for item in versions if item.version == version), None)
|
|
321
|
+
if projection_version is None:
|
|
322
|
+
return {}
|
|
323
|
+
|
|
324
|
+
aliases: dict[str, tuple[str, str, int]] = {}
|
|
325
|
+
for source_ref in [projection_version.source, *projection_version.joins]:
|
|
326
|
+
try:
|
|
327
|
+
resolved = resolve_model_ref(workspace.mdl, source_ref.model, source_ref.version)
|
|
328
|
+
except LookupError:
|
|
329
|
+
continue
|
|
330
|
+
aliases[source_ref.alias] = (
|
|
331
|
+
resolved.domain_name,
|
|
332
|
+
resolved.model_name,
|
|
333
|
+
resolved.version.version,
|
|
334
|
+
)
|
|
335
|
+
return aliases
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _field_exists(workspace, domain_name: str, model_name: str, version: int, field_name: str) -> bool:
|
|
339
|
+
source_version = _source_version(workspace, domain_name, model_name, version)
|
|
340
|
+
if source_version is None:
|
|
341
|
+
return False
|
|
342
|
+
return any(field.name == field_name for field in getattr(source_version, "fields", []))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _source_version(workspace, domain_name: str, model_name: str, version: int):
|
|
346
|
+
domain = next((item for item in workspace.mdl.domains if item.name == domain_name), None)
|
|
347
|
+
if domain is None:
|
|
348
|
+
return None
|
|
349
|
+
versions = domain.models.get(model_name, [])
|
|
350
|
+
source_version = next((item for item in versions if item.version == version), None)
|
|
351
|
+
if source_version is not None:
|
|
352
|
+
return source_version
|
|
353
|
+
versions = domain.projections.get(model_name, [])
|
|
354
|
+
return next((item for item in versions if item.version == version), None)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _find_source_field_location(
|
|
358
|
+
workspace,
|
|
359
|
+
domain_name: str,
|
|
360
|
+
model_name: str,
|
|
361
|
+
version: int,
|
|
362
|
+
field_name: str,
|
|
363
|
+
) -> types.Location | None:
|
|
364
|
+
for kind in ("model", "projection"):
|
|
365
|
+
location = _find_field_location(
|
|
366
|
+
workspace,
|
|
367
|
+
domain_name,
|
|
368
|
+
kind,
|
|
369
|
+
model_name,
|
|
370
|
+
version,
|
|
371
|
+
field_name,
|
|
372
|
+
)
|
|
373
|
+
if location is not None:
|
|
374
|
+
return location
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _find_decl_location(
|
|
379
|
+
workspace,
|
|
380
|
+
domain_name: str,
|
|
381
|
+
kind: str,
|
|
382
|
+
name: str,
|
|
383
|
+
version: int,
|
|
384
|
+
) -> types.Location | None:
|
|
385
|
+
for source in workspace.sources:
|
|
386
|
+
current_domain: str | None = None
|
|
387
|
+
lines = source.text.splitlines()
|
|
388
|
+
for line_no, line_text in enumerate(lines):
|
|
389
|
+
domain_match = _DOMAIN_PATTERN.match(line_text)
|
|
390
|
+
if domain_match:
|
|
391
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
392
|
+
continue
|
|
393
|
+
decl_match = _DECL_PATTERN.match(line_text)
|
|
394
|
+
if not decl_match or current_domain != domain_name:
|
|
395
|
+
continue
|
|
396
|
+
decl_kind = decl_match.group("kind")
|
|
397
|
+
if kind == "model":
|
|
398
|
+
if decl_kind not in _MODEL_DECL_KINDS:
|
|
399
|
+
continue
|
|
400
|
+
elif decl_kind != kind:
|
|
401
|
+
continue
|
|
402
|
+
if decl_match.group("name") != name:
|
|
403
|
+
continue
|
|
404
|
+
if int(decl_match.group("version")) != version:
|
|
405
|
+
continue
|
|
406
|
+
return types.Location(
|
|
407
|
+
uri=source.uri,
|
|
408
|
+
range=types.Range(
|
|
409
|
+
start=types.Position(line=line_no, character=decl_match.start("name")),
|
|
410
|
+
end=types.Position(line=line_no, character=decl_match.end("name")),
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _find_field_location(
|
|
417
|
+
workspace,
|
|
418
|
+
domain_name: str,
|
|
419
|
+
kind: str,
|
|
420
|
+
name: str,
|
|
421
|
+
version: int,
|
|
422
|
+
field_name: str,
|
|
423
|
+
) -> types.Location | None:
|
|
424
|
+
for source in workspace.sources:
|
|
425
|
+
current_domain: str | None = None
|
|
426
|
+
active = False
|
|
427
|
+
lines = source.text.splitlines()
|
|
428
|
+
pattern = _MODEL_FIELD_PATTERN if kind == "model" else _PROJECTION_FIELD_PATTERN
|
|
429
|
+
|
|
430
|
+
for line_no, line_text in enumerate(lines):
|
|
431
|
+
domain_match = _DOMAIN_PATTERN.match(line_text)
|
|
432
|
+
if domain_match:
|
|
433
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
434
|
+
active = False
|
|
435
|
+
continue
|
|
436
|
+
decl_match = _DECL_PATTERN.match(line_text)
|
|
437
|
+
if decl_match and current_domain == domain_name:
|
|
438
|
+
decl_kind = decl_match.group("kind")
|
|
439
|
+
active = (
|
|
440
|
+
(decl_kind in _MODEL_DECL_KINDS if kind == "model" else decl_kind == kind)
|
|
441
|
+
and decl_match.group("name") == name
|
|
442
|
+
and int(decl_match.group("version")) == version
|
|
443
|
+
)
|
|
444
|
+
continue
|
|
445
|
+
if not active:
|
|
446
|
+
continue
|
|
447
|
+
field_match = pattern.match(line_text)
|
|
448
|
+
if field_match and field_match.group("name") == field_name:
|
|
449
|
+
return types.Location(
|
|
450
|
+
uri=source.uri,
|
|
451
|
+
range=types.Range(
|
|
452
|
+
start=types.Position(line=line_no, character=field_match.start("name")),
|
|
453
|
+
end=types.Position(line=line_no, character=field_match.end("name")),
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
if line_text.strip() == "}":
|
|
457
|
+
active = False
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _dedupe_locations(locations: list[types.Location]) -> list[types.Location]:
|
|
462
|
+
seen: set[tuple[str, int, int, int, int]] = set()
|
|
463
|
+
deduped: list[types.Location] = []
|
|
464
|
+
for location in locations:
|
|
465
|
+
key = (
|
|
466
|
+
location.uri,
|
|
467
|
+
location.range.start.line,
|
|
468
|
+
location.range.start.character,
|
|
469
|
+
location.range.end.line,
|
|
470
|
+
location.range.end.character,
|
|
471
|
+
)
|
|
472
|
+
if key in seen:
|
|
473
|
+
continue
|
|
474
|
+
seen.add(key)
|
|
475
|
+
deduped.append(location)
|
|
476
|
+
return deduped
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _current_scope(text: str, line: int) -> tuple[str, str, str, int] | None:
|
|
480
|
+
lines = text.splitlines()
|
|
481
|
+
current_domain: str | None = None
|
|
482
|
+
current_kind: str | None = None
|
|
483
|
+
current_name: str | None = None
|
|
484
|
+
current_version: int | None = None
|
|
485
|
+
for item in lines[: line + 1]:
|
|
486
|
+
domain_match = _DOMAIN_PATTERN.match(item)
|
|
487
|
+
if domain_match:
|
|
488
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
489
|
+
current_kind = None
|
|
490
|
+
current_name = None
|
|
491
|
+
current_version = None
|
|
492
|
+
continue
|
|
493
|
+
decl_match = _DECL_PATTERN.match(item)
|
|
494
|
+
if decl_match and current_domain is not None:
|
|
495
|
+
current_kind = "model" if decl_match.group("kind") != "projection" else "projection"
|
|
496
|
+
current_name = decl_match.group("name")
|
|
497
|
+
current_version = int(decl_match.group("version"))
|
|
498
|
+
if current_domain and current_kind and current_name and current_version is not None:
|
|
499
|
+
return current_domain, current_kind, current_name, current_version
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _word_at(text_line: str, character: int) -> str | None:
|
|
504
|
+
for match in _WORD_PATTERN.finditer(text_line):
|
|
505
|
+
if _contains(match.start(), match.end(), character):
|
|
506
|
+
return match.group(0)
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _contains(start: int, end: int, character: int) -> bool:
|
|
511
|
+
return start <= character <= max(end - 1, start)
|