apparmor-language-server 0.5.1__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.
- apparmor_language_server/__init__.py +23 -0
- apparmor_language_server/__main__.py +24 -0
- apparmor_language_server/_text.py +70 -0
- apparmor_language_server/call_hierarchy.py +300 -0
- apparmor_language_server/code_actions.py +307 -0
- apparmor_language_server/codes.py +92 -0
- apparmor_language_server/completions.py +615 -0
- apparmor_language_server/constants.py +1243 -0
- apparmor_language_server/diagnostics.py +1771 -0
- apparmor_language_server/docs.py +249 -0
- apparmor_language_server/format.py +281 -0
- apparmor_language_server/formatting.py +727 -0
- apparmor_language_server/hover.py +822 -0
- apparmor_language_server/indexer.py +127 -0
- apparmor_language_server/lint.py +361 -0
- apparmor_language_server/nodes.py +626 -0
- apparmor_language_server/parser.py +1905 -0
- apparmor_language_server/render.py +476 -0
- apparmor_language_server/semantic_tokens.py +1250 -0
- apparmor_language_server/server.py +1716 -0
- apparmor_language_server-0.5.1.dist-info/METADATA +634 -0
- apparmor_language_server-0.5.1.dist-info/RECORD +26 -0
- apparmor_language_server-0.5.1.dist-info/WHEEL +5 -0
- apparmor_language_server-0.5.1.dist-info/entry_points.txt +4 -0
- apparmor_language_server-0.5.1.dist-info/licenses/LICENSE +674 -0
- apparmor_language_server-0.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# apparmor-language-server - LSP server for AppArmor profiles
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 Alex Murray <murray.alex@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
"""AppArmor Language Server Protocol server."""
|
|
20
|
+
|
|
21
|
+
from importlib.metadata import version
|
|
22
|
+
|
|
23
|
+
__version__ = version("apparmor-language-server")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# apparmor-language-server - LSP server for AppArmor profiles
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 Alex Murray <murray.alex@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
"""Entry point: python -m apparmor_language_server [--tcp] [--host HOST] [--port PORT]"""
|
|
20
|
+
|
|
21
|
+
from .server import main
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# apparmor-language-server - LSP server for AppArmor profiles
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 Alex Murray <murray.alex@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
AppArmor LSP – shared line/text helpers.
|
|
21
|
+
|
|
22
|
+
Tiny module so that the server, parser and diagnostics layers share one
|
|
23
|
+
implementation of "where does a comment start on this line?" rather than
|
|
24
|
+
each carrying its own copy.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
# Lines starting with a directive (#include, #abi) use '#' as a directive
|
|
32
|
+
# marker, not a comment introducer.
|
|
33
|
+
_RE_DIRECTIVE_LINE = re.compile(r"^\s*#(include|abi)\b")
|
|
34
|
+
|
|
35
|
+
# Matches either a quoted string (to skip over its contents) or a '#' that
|
|
36
|
+
# starts a comment — i.e. one at the start of the line or immediately preceded
|
|
37
|
+
# by whitespace or a comma. The comment '#' is captured in group 1; a quoted-
|
|
38
|
+
# string match leaves group 1 empty so the caller can distinguish the two.
|
|
39
|
+
_RE_COMMENT_OR_QUOTED = re.compile(r'"[^"]*"|\'[^\']*\'|((?:^|(?<=[ \t,]))#)')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def raw_to_pos(raw: str, start_line: int, offset: int) -> tuple[int, int]:
|
|
43
|
+
"""Convert a byte offset within *raw* to an absolute ``(line, character)`` pair.
|
|
44
|
+
|
|
45
|
+
*raw* may span multiple source lines joined with ``\\n``; each ``\\n``
|
|
46
|
+
advances the line counter. The character is the column of *offset*
|
|
47
|
+
within its source line (0-based, absolute from the left margin).
|
|
48
|
+
"""
|
|
49
|
+
prefix = raw[:offset]
|
|
50
|
+
nl_count = prefix.count("\n")
|
|
51
|
+
if nl_count:
|
|
52
|
+
char = offset - prefix.rfind("\n") - 1
|
|
53
|
+
else:
|
|
54
|
+
char = offset
|
|
55
|
+
return start_line + nl_count, char
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def code_end(line: str) -> int:
|
|
59
|
+
"""Return the column at which a trailing comment begins on *line*.
|
|
60
|
+
|
|
61
|
+
Returns ``len(line)`` if the line has no trailing comment, or if the
|
|
62
|
+
line is itself a directive (``#include``/``#abi``) where '#' is the
|
|
63
|
+
directive marker rather than a comment introducer.
|
|
64
|
+
"""
|
|
65
|
+
if _RE_DIRECTIVE_LINE.match(line):
|
|
66
|
+
return len(line)
|
|
67
|
+
for m in _RE_COMMENT_OR_QUOTED.finditer(line):
|
|
68
|
+
if m.group(1) is not None:
|
|
69
|
+
return m.start(1)
|
|
70
|
+
return len(line)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# apparmor-language-server - LSP server for AppArmor profiles
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 Alex Murray <murray.alex@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
AppArmor LSP – call hierarchy provider.
|
|
21
|
+
|
|
22
|
+
Implements ``textDocument/prepareCallHierarchy``,
|
|
23
|
+
``callHierarchy/incomingCalls``, and ``callHierarchy/outgoingCalls``.
|
|
24
|
+
|
|
25
|
+
Incoming calls: which profiles exec-transition (``px``, ``cx``, ``Px``, …),
|
|
26
|
+
``change_profile``, or ``change_hat`` into a given profile?
|
|
27
|
+
|
|
28
|
+
Outgoing calls: which profiles does a given profile exec-transition,
|
|
29
|
+
``change_profile``, or ``change_hat`` to?
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
from typing import Iterable, Iterator, Optional, Union
|
|
36
|
+
|
|
37
|
+
from lsprotocol.types import (
|
|
38
|
+
CallHierarchyIncomingCall,
|
|
39
|
+
CallHierarchyItem,
|
|
40
|
+
CallHierarchyOutgoingCall,
|
|
41
|
+
Position,
|
|
42
|
+
Range,
|
|
43
|
+
SymbolKind,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from .constants import translate_uri
|
|
47
|
+
from .nodes import (
|
|
48
|
+
BlockNode,
|
|
49
|
+
ChangeHatRuleNode,
|
|
50
|
+
ChangeProfileRuleNode,
|
|
51
|
+
DocumentNode,
|
|
52
|
+
FileRuleNode,
|
|
53
|
+
IfBlockNode,
|
|
54
|
+
Node,
|
|
55
|
+
ProfileNode,
|
|
56
|
+
Token,
|
|
57
|
+
iter_children,
|
|
58
|
+
walk,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
# Type alias for the cache snapshot taken under the lock.
|
|
64
|
+
_CacheSnapshot = list[tuple[str, tuple[DocumentNode, list]]]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _unquote(tok: Token) -> str:
|
|
71
|
+
"""Strip surrounding double-quotes from a token value.
|
|
72
|
+
|
|
73
|
+
The parser preserves quotes on ``change_hat`` hat names and
|
|
74
|
+
``change_profile`` targets (e.g. ``change_hat "inner"`` →
|
|
75
|
+
``Token('"inner"', …)``). Profile names are stored without quotes, so
|
|
76
|
+
the quotes must be stripped before doing a name lookup.
|
|
77
|
+
"""
|
|
78
|
+
s = str(tok)
|
|
79
|
+
return s[1:-1] if len(s) >= 2 and s[0] == '"' and s[-1] == '"' else s
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _profile_name(profile: ProfileNode) -> str:
|
|
83
|
+
tok = profile.name or profile.attachment
|
|
84
|
+
return str(tok) if tok is not None else "(anonymous)"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _make_item(profile: ProfileNode, uri: str) -> CallHierarchyItem:
|
|
88
|
+
name = _profile_name(profile)
|
|
89
|
+
tok = profile.name or profile.attachment
|
|
90
|
+
sel_range = tok.range if tok is not None else profile.range
|
|
91
|
+
return CallHierarchyItem(
|
|
92
|
+
name=name,
|
|
93
|
+
kind=SymbolKind.Class,
|
|
94
|
+
uri=translate_uri(uri),
|
|
95
|
+
range=profile.range,
|
|
96
|
+
selection_range=sel_range,
|
|
97
|
+
data=name,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _direct_transitions(
|
|
102
|
+
nodes: Iterable[Node],
|
|
103
|
+
) -> Iterator[tuple[Node, str, Token]]:
|
|
104
|
+
"""Yield ``(rule, target_str, target_token)`` for every exec-transition rule
|
|
105
|
+
in *nodes*.
|
|
106
|
+
|
|
107
|
+
Recurses into ``IfBlockNode`` / ``QualifierBlockNode`` (same security
|
|
108
|
+
domain) but does *not* descend into nested ``ProfileNode`` hat children
|
|
109
|
+
(they are separate security domains and appear as separate call hierarchy
|
|
110
|
+
items).
|
|
111
|
+
"""
|
|
112
|
+
for node in nodes:
|
|
113
|
+
if isinstance(node, ProfileNode):
|
|
114
|
+
continue # hat — treat as a separate profile, not a recursive descent
|
|
115
|
+
if isinstance(node, FileRuleNode):
|
|
116
|
+
tgt = node.profile_transition_target
|
|
117
|
+
if tgt is not None:
|
|
118
|
+
yield node, str(tgt), tgt
|
|
119
|
+
elif (
|
|
120
|
+
isinstance(node, ChangeProfileRuleNode) and node.target_profile is not None
|
|
121
|
+
):
|
|
122
|
+
yield node, _unquote(node.target_profile), node.target_profile
|
|
123
|
+
elif isinstance(node, ChangeHatRuleNode):
|
|
124
|
+
for hat in node.hats:
|
|
125
|
+
yield node, _unquote(hat), hat
|
|
126
|
+
elif isinstance(node, BlockNode):
|
|
127
|
+
yield from _direct_transitions(node.children)
|
|
128
|
+
if isinstance(node, IfBlockNode) and node.else_branch is not None:
|
|
129
|
+
yield from _direct_transitions([node.else_branch])
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _all_transitions(
|
|
133
|
+
node: Union[Node, DocumentNode],
|
|
134
|
+
enclosing: Optional[ProfileNode] = None,
|
|
135
|
+
) -> Iterator[tuple[ProfileNode, Node, str, Token]]:
|
|
136
|
+
"""Yield ``(enclosing_profile, rule, target_str, target_token)`` for every
|
|
137
|
+
exec-transition rule in the subtree of *node*.
|
|
138
|
+
|
|
139
|
+
The *enclosing_profile* is the most specific ``ProfileNode`` ancestor
|
|
140
|
+
(outer profile or hat) so that incoming-call items are attributed to the
|
|
141
|
+
correct caller.
|
|
142
|
+
"""
|
|
143
|
+
if isinstance(node, DocumentNode):
|
|
144
|
+
for child in node.children:
|
|
145
|
+
yield from _all_transitions(child, None)
|
|
146
|
+
return
|
|
147
|
+
if isinstance(node, ProfileNode):
|
|
148
|
+
enclosing = node
|
|
149
|
+
elif enclosing is not None:
|
|
150
|
+
if isinstance(node, FileRuleNode):
|
|
151
|
+
tgt = node.profile_transition_target
|
|
152
|
+
if tgt is not None:
|
|
153
|
+
yield enclosing, node, str(tgt), tgt
|
|
154
|
+
elif (
|
|
155
|
+
isinstance(node, ChangeProfileRuleNode) and node.target_profile is not None
|
|
156
|
+
):
|
|
157
|
+
yield enclosing, node, _unquote(node.target_profile), node.target_profile
|
|
158
|
+
elif isinstance(node, ChangeHatRuleNode):
|
|
159
|
+
for hat in node.hats:
|
|
160
|
+
yield enclosing, node, _unquote(hat), hat
|
|
161
|
+
for child in iter_children(node):
|
|
162
|
+
yield from _all_transitions(child, enclosing)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _find_profile(
|
|
166
|
+
cache_snapshot: _CacheSnapshot,
|
|
167
|
+
name: str,
|
|
168
|
+
) -> Optional[tuple[ProfileNode, str]]:
|
|
169
|
+
"""Find a ``ProfileNode`` by name or attachment across all cached documents."""
|
|
170
|
+
for doc_uri, (doc, _) in cache_snapshot:
|
|
171
|
+
for node in walk(doc):
|
|
172
|
+
if isinstance(node, ProfileNode):
|
|
173
|
+
if node.name == name or node.attachment == name:
|
|
174
|
+
return node, doc_uri
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def prepare_call_hierarchy(
|
|
182
|
+
doc: DocumentNode,
|
|
183
|
+
uri: str,
|
|
184
|
+
line: int,
|
|
185
|
+
ch: int,
|
|
186
|
+
cache_snapshot: _CacheSnapshot,
|
|
187
|
+
) -> Optional[list[CallHierarchyItem]]:
|
|
188
|
+
"""Return a ``CallHierarchyItem`` for the profile under the cursor, or ``None``.
|
|
189
|
+
|
|
190
|
+
Two positions resolve to a call hierarchy item:
|
|
191
|
+
- A profile name or attachment token — the item is that profile itself.
|
|
192
|
+
- An exec-transition target token — the item is the target profile (looked
|
|
193
|
+
up across the full workspace cache).
|
|
194
|
+
"""
|
|
195
|
+
# Cursor on a profile name or attachment token
|
|
196
|
+
for node in walk(doc):
|
|
197
|
+
if not isinstance(node, ProfileNode):
|
|
198
|
+
continue
|
|
199
|
+
for tok in (node.name, node.attachment):
|
|
200
|
+
if (
|
|
201
|
+
tok is not None
|
|
202
|
+
and tok.range.start.line == line
|
|
203
|
+
and tok.range.start.character <= ch <= tok.range.end.character
|
|
204
|
+
):
|
|
205
|
+
return [_make_item(node, uri)]
|
|
206
|
+
|
|
207
|
+
# Cursor on an exec-transition target token
|
|
208
|
+
for node in walk(doc):
|
|
209
|
+
if node.range.start.line != line:
|
|
210
|
+
continue
|
|
211
|
+
target_str: Optional[str] = None
|
|
212
|
+
if isinstance(node, FileRuleNode):
|
|
213
|
+
t = node.profile_transition_target
|
|
214
|
+
if t is not None and t.range.start.character <= ch <= t.range.end.character:
|
|
215
|
+
target_str = str(t)
|
|
216
|
+
elif (
|
|
217
|
+
isinstance(node, ChangeProfileRuleNode) and node.target_profile is not None
|
|
218
|
+
):
|
|
219
|
+
t = node.target_profile
|
|
220
|
+
if t.range.start.character <= ch <= t.range.end.character:
|
|
221
|
+
target_str = _unquote(t)
|
|
222
|
+
elif isinstance(node, ChangeHatRuleNode):
|
|
223
|
+
for hat in node.hats:
|
|
224
|
+
if hat.range.start.character <= ch <= hat.range.end.character:
|
|
225
|
+
target_str = _unquote(hat)
|
|
226
|
+
break
|
|
227
|
+
if target_str is not None:
|
|
228
|
+
found = _find_profile(cache_snapshot, target_str)
|
|
229
|
+
if found is not None:
|
|
230
|
+
return [_make_item(found[0], found[1])]
|
|
231
|
+
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_incoming_calls(
|
|
236
|
+
cache_snapshot: _CacheSnapshot,
|
|
237
|
+
item: CallHierarchyItem,
|
|
238
|
+
) -> list[CallHierarchyIncomingCall]:
|
|
239
|
+
"""Return all profiles that exec-transition into the profile named by *item*.
|
|
240
|
+
|
|
241
|
+
Multiple rules from the same caller profile are merged into a single
|
|
242
|
+
``CallHierarchyIncomingCall`` with all rule locations listed in
|
|
243
|
+
``from_ranges``.
|
|
244
|
+
"""
|
|
245
|
+
target_name: str = item.data if isinstance(item.data, str) else item.name
|
|
246
|
+
# key: "<uri>::<profile_name>" → (caller_item, [from_ranges])
|
|
247
|
+
callers: dict[str, tuple[CallHierarchyItem, list[Range]]] = {}
|
|
248
|
+
for doc_uri, (doc, _) in cache_snapshot:
|
|
249
|
+
for profile, _rule, tgt_str, tgt_tok in _all_transitions(doc):
|
|
250
|
+
if tgt_str != target_name:
|
|
251
|
+
continue
|
|
252
|
+
key = f"{doc_uri}::{_profile_name(profile)}"
|
|
253
|
+
if key not in callers:
|
|
254
|
+
callers[key] = (_make_item(profile, doc_uri), [])
|
|
255
|
+
callers[key][1].append(tgt_tok.range)
|
|
256
|
+
return [
|
|
257
|
+
CallHierarchyIncomingCall(from_=caller_item, from_ranges=ranges)
|
|
258
|
+
for caller_item, ranges in callers.values()
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_outgoing_calls(
|
|
263
|
+
cache_snapshot: _CacheSnapshot,
|
|
264
|
+
item: CallHierarchyItem,
|
|
265
|
+
) -> list[CallHierarchyOutgoingCall]:
|
|
266
|
+
"""Return all profiles that the profile named by *item* exec-transitions to.
|
|
267
|
+
|
|
268
|
+
Multiple rules pointing to the same target profile are merged into a
|
|
269
|
+
single ``CallHierarchyOutgoingCall`` with all rule locations in
|
|
270
|
+
``from_ranges``.
|
|
271
|
+
"""
|
|
272
|
+
source_name: str = item.data if isinstance(item.data, str) else item.name
|
|
273
|
+
found = _find_profile(cache_snapshot, source_name)
|
|
274
|
+
if found is None:
|
|
275
|
+
return []
|
|
276
|
+
source_profile, source_uri = found
|
|
277
|
+
|
|
278
|
+
# key: target profile name → (target_item, [from_ranges])
|
|
279
|
+
targets: dict[str, tuple[CallHierarchyItem, list[Range]]] = {}
|
|
280
|
+
for _rule, tgt_str, tgt_tok in _direct_transitions(source_profile.children):
|
|
281
|
+
tgt_found = _find_profile(cache_snapshot, tgt_str)
|
|
282
|
+
if tgt_found is not None:
|
|
283
|
+
tgt_item = _make_item(tgt_found[0], tgt_found[1])
|
|
284
|
+
else:
|
|
285
|
+
# Target not in cache — synthesise a placeholder item.
|
|
286
|
+
tgt_item = CallHierarchyItem(
|
|
287
|
+
name=tgt_str,
|
|
288
|
+
kind=SymbolKind.Class,
|
|
289
|
+
uri=translate_uri(source_uri),
|
|
290
|
+
range=Range(Position(0, 0), Position(0, 0)),
|
|
291
|
+
selection_range=Range(Position(0, 0), Position(0, 0)),
|
|
292
|
+
data=tgt_str,
|
|
293
|
+
)
|
|
294
|
+
if tgt_str not in targets:
|
|
295
|
+
targets[tgt_str] = (tgt_item, [])
|
|
296
|
+
targets[tgt_str][1].append(tgt_tok.range)
|
|
297
|
+
return [
|
|
298
|
+
CallHierarchyOutgoingCall(to=tgt_item, from_ranges=ranges)
|
|
299
|
+
for tgt_item, ranges in targets.values()
|
|
300
|
+
]
|