agent-codinglanguage-mapper 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.
- agent_codinglanguage_mapper/__init__.py +20 -0
- agent_codinglanguage_mapper/__main__.py +4 -0
- agent_codinglanguage_mapper/_version.py +2 -0
- agent_codinglanguage_mapper/adapters/__init__.py +4 -0
- agent_codinglanguage_mapper/adapters/delphi.py +822 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
- agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
- agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
- agent_codinglanguage_mapper/cli.py +138 -0
- agent_codinglanguage_mapper/discovery.py +1328 -0
- agent_codinglanguage_mapper/indexer.py +1102 -0
- agent_codinglanguage_mapper/integration.py +186 -0
- agent_codinglanguage_mapper/lsp_server.py +413 -0
- agent_codinglanguage_mapper/lsp_service.py +447 -0
- agent_codinglanguage_mapper/mapper.py +596 -0
- agent_codinglanguage_mapper/models.py +101 -0
- agent_codinglanguage_mapper/protocol.py +152 -0
- agent_codinglanguage_mapper/rendering.py +153 -0
- agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
- agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
- agent_codinglanguage_mapper/worker.py +29 -0
- agent_codinglanguage_mapper/workspace.py +114 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
- agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
from .source_reader import read_source_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
IncludeLoader = Callable[[str, str], Optional[tuple[str, str]]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class SourceMapEntry:
|
|
15
|
+
file_name: str
|
|
16
|
+
line: int
|
|
17
|
+
col_offset: int = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class SourceMapSegment:
|
|
22
|
+
output_col: int
|
|
23
|
+
file_name: str
|
|
24
|
+
line: int
|
|
25
|
+
source_col: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class PreprocessorProblem:
|
|
30
|
+
kind: str
|
|
31
|
+
message: str
|
|
32
|
+
file_name: str
|
|
33
|
+
line: int
|
|
34
|
+
col: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CommentInfo:
|
|
39
|
+
kind: str
|
|
40
|
+
text: str
|
|
41
|
+
file_name: str
|
|
42
|
+
line: int
|
|
43
|
+
col: int
|
|
44
|
+
end_line: int
|
|
45
|
+
end_col: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class PreprocessedSource:
|
|
50
|
+
text: str
|
|
51
|
+
source_map: list[SourceMapEntry]
|
|
52
|
+
defines: set[str]
|
|
53
|
+
scoped_enums: bool
|
|
54
|
+
problems: list[PreprocessorProblem]
|
|
55
|
+
comments: list[CommentInfo]
|
|
56
|
+
included_files: tuple[str, ...] = ()
|
|
57
|
+
source_segments: tuple[tuple[SourceMapSegment, ...], ...] = ()
|
|
58
|
+
|
|
59
|
+
def map_position(self, line: int, col: int) -> tuple[str, int, int]:
|
|
60
|
+
if line < 1 or line > len(self.source_map):
|
|
61
|
+
return ('', 0, 0)
|
|
62
|
+
entry = self.source_map[line - 1]
|
|
63
|
+
if line <= len(self.source_segments):
|
|
64
|
+
selected: SourceMapSegment | None = None
|
|
65
|
+
for segment in self.source_segments[line - 1]:
|
|
66
|
+
if segment.output_col > col:
|
|
67
|
+
break
|
|
68
|
+
selected = segment
|
|
69
|
+
if selected is not None:
|
|
70
|
+
return (
|
|
71
|
+
selected.file_name,
|
|
72
|
+
selected.line,
|
|
73
|
+
selected.source_col + col - selected.output_col,
|
|
74
|
+
)
|
|
75
|
+
return (entry.file_name, entry.line, col + entry.col_offset)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class PreprocessorOptions:
|
|
80
|
+
use_defines: bool = True
|
|
81
|
+
compiler_version: float = 36.0
|
|
82
|
+
rtl_version: float = 36.0
|
|
83
|
+
scoped_enums: bool = False
|
|
84
|
+
option_states: dict[str, bool] = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class _ConditionalFrame:
|
|
89
|
+
parent_active: bool
|
|
90
|
+
branch_taken: bool
|
|
91
|
+
current_active: bool
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class _FileContext:
|
|
96
|
+
text: str
|
|
97
|
+
file_name: str
|
|
98
|
+
index: int = 0
|
|
99
|
+
line: int = 1
|
|
100
|
+
col: int = 1
|
|
101
|
+
pending_output: str = ''
|
|
102
|
+
pending_line: int = 1
|
|
103
|
+
pending_col: int = 1
|
|
104
|
+
|
|
105
|
+
def peek(self, offset: int = 0) -> str:
|
|
106
|
+
idx = self.index + offset
|
|
107
|
+
if idx < 0 or idx >= len(self.text):
|
|
108
|
+
return ''
|
|
109
|
+
return self.text[idx]
|
|
110
|
+
|
|
111
|
+
def advance(self, count: int = 1) -> str:
|
|
112
|
+
if count <= 0:
|
|
113
|
+
return ''
|
|
114
|
+
end = min(len(self.text), self.index + count)
|
|
115
|
+
chunk = self.text[self.index:end]
|
|
116
|
+
for ch in chunk:
|
|
117
|
+
if ch == '\n':
|
|
118
|
+
self.line += 1
|
|
119
|
+
self.col = 1
|
|
120
|
+
else:
|
|
121
|
+
self.col += 1
|
|
122
|
+
self.index = end
|
|
123
|
+
return chunk
|
|
124
|
+
|
|
125
|
+
def eof(self) -> bool:
|
|
126
|
+
return self.index >= len(self.text)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _OutputBuffer:
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
self._lines: list[str] = ['']
|
|
132
|
+
self._line_map: list[SourceMapEntry] = []
|
|
133
|
+
self._line_segments: list[list[SourceMapSegment]] = [[]]
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def text(self) -> str:
|
|
137
|
+
return '\n'.join(self._lines)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def line_map(self) -> list[SourceMapEntry]:
|
|
141
|
+
return self._line_map
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def line_segments(self) -> tuple[tuple[SourceMapSegment, ...], ...]:
|
|
145
|
+
return tuple(tuple(line) for line in self._line_segments)
|
|
146
|
+
|
|
147
|
+
def append_char(self, ch: str, file_name: str, line: int, source_col: int) -> None:
|
|
148
|
+
self._ensure_mapping(file_name, line)
|
|
149
|
+
output_col = len(self._lines[-1])
|
|
150
|
+
segments = self._line_segments[-1]
|
|
151
|
+
if not segments:
|
|
152
|
+
segments.append(SourceMapSegment(output_col, file_name, line, source_col))
|
|
153
|
+
else:
|
|
154
|
+
previous = segments[-1]
|
|
155
|
+
expected_col = previous.source_col + output_col - previous.output_col
|
|
156
|
+
if (
|
|
157
|
+
previous.file_name != file_name
|
|
158
|
+
or previous.line != line
|
|
159
|
+
or expected_col != source_col
|
|
160
|
+
):
|
|
161
|
+
segments.append(SourceMapSegment(output_col, file_name, line, source_col))
|
|
162
|
+
if ch == '\n':
|
|
163
|
+
self._lines.append('')
|
|
164
|
+
self._line_segments.append([])
|
|
165
|
+
else:
|
|
166
|
+
self._lines[-1] += ch
|
|
167
|
+
|
|
168
|
+
def _ensure_mapping(self, file_name: str, line: int) -> None:
|
|
169
|
+
if len(self._line_map) < len(self._lines):
|
|
170
|
+
self._line_map.append(SourceMapEntry(file_name=file_name, line=line, col_offset=0))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Preprocessor:
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
*,
|
|
177
|
+
defines: Iterable[str] = (),
|
|
178
|
+
include_paths: Iterable[str] = (),
|
|
179
|
+
include_loader: Optional[IncludeLoader] = None,
|
|
180
|
+
workspace_root: str | Path | None = None,
|
|
181
|
+
options: Optional[PreprocessorOptions] = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
self.defines: set[str] = {self._normalize_define(d) for d in defines if d}
|
|
184
|
+
self.include_paths = [Path(p) for p in include_paths]
|
|
185
|
+
self.workspace_root = (
|
|
186
|
+
Path(workspace_root).expanduser().resolve() if workspace_root is not None else None
|
|
187
|
+
)
|
|
188
|
+
self._uses_default_include_loader = include_loader is None
|
|
189
|
+
self.include_loader = include_loader or self._default_include_loader
|
|
190
|
+
self.options = options or PreprocessorOptions()
|
|
191
|
+
self._apply_default_compiler_defines()
|
|
192
|
+
self.scoped_enums = self.options.scoped_enums
|
|
193
|
+
self._option_values: dict[str, str] = {}
|
|
194
|
+
self._option_stack: list[tuple[dict[str, str], bool]] = []
|
|
195
|
+
self._problems: list[PreprocessorProblem] = []
|
|
196
|
+
self._comments: list[CommentInfo] = []
|
|
197
|
+
self._conditional_stack: list[_ConditionalFrame] = []
|
|
198
|
+
self._active_workspace_root: Path | None = None
|
|
199
|
+
self._last_include_error = ""
|
|
200
|
+
self._included_files: list[str] = []
|
|
201
|
+
|
|
202
|
+
def process(self, text: str, file_name: str) -> PreprocessedSource:
|
|
203
|
+
self._problems = []
|
|
204
|
+
self._comments = []
|
|
205
|
+
self._conditional_stack = []
|
|
206
|
+
self._active_workspace_root = self.workspace_root or Path(file_name).expanduser().resolve().parent
|
|
207
|
+
self._last_include_error = ""
|
|
208
|
+
self._included_files = []
|
|
209
|
+
self._option_stack = []
|
|
210
|
+
self.scoped_enums = self.options.scoped_enums
|
|
211
|
+
self._option_values = {
|
|
212
|
+
self._normalize_option_name(name): 'ON' if bool(value) else 'OFF'
|
|
213
|
+
for name, value in self.options.option_states.items()
|
|
214
|
+
}
|
|
215
|
+
if 'SCOPEDENUMS' in self._option_values:
|
|
216
|
+
self.scoped_enums = self._option_values['SCOPEDENUMS'] == 'ON'
|
|
217
|
+
else:
|
|
218
|
+
self._set_option('SCOPEDENUMS', 'ON' if self.scoped_enums else 'OFF')
|
|
219
|
+
|
|
220
|
+
normalized = self._normalize_newlines(text)
|
|
221
|
+
contexts = [_FileContext(text=normalized, file_name=file_name)]
|
|
222
|
+
include_stack = [file_name]
|
|
223
|
+
output = _OutputBuffer()
|
|
224
|
+
|
|
225
|
+
while contexts:
|
|
226
|
+
ctx = contexts[-1]
|
|
227
|
+
|
|
228
|
+
if ctx.pending_output:
|
|
229
|
+
pending = ctx.pending_output
|
|
230
|
+
ctx.pending_output = ''
|
|
231
|
+
pending_line = ctx.pending_line
|
|
232
|
+
pending_col = ctx.pending_col
|
|
233
|
+
ctx.pending_line = ctx.line
|
|
234
|
+
ctx.pending_col = ctx.col
|
|
235
|
+
self._emit_text(
|
|
236
|
+
output,
|
|
237
|
+
pending,
|
|
238
|
+
ctx.file_name,
|
|
239
|
+
pending_line,
|
|
240
|
+
col=pending_col,
|
|
241
|
+
active=True,
|
|
242
|
+
)
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
if ctx.eof():
|
|
246
|
+
contexts.pop()
|
|
247
|
+
if include_stack:
|
|
248
|
+
include_stack.pop()
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
ch = ctx.peek()
|
|
252
|
+
|
|
253
|
+
if ch == "'":
|
|
254
|
+
start_line = ctx.line
|
|
255
|
+
start_col = ctx.col
|
|
256
|
+
active = self._is_active()
|
|
257
|
+
literal = self._consume_string(ctx)
|
|
258
|
+
self._emit_text(
|
|
259
|
+
output,
|
|
260
|
+
literal,
|
|
261
|
+
ctx.file_name,
|
|
262
|
+
start_line,
|
|
263
|
+
col=start_col,
|
|
264
|
+
active=active,
|
|
265
|
+
)
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
if ch == '/' and ctx.peek(1) == '/':
|
|
269
|
+
start_line = ctx.line
|
|
270
|
+
start_col = ctx.col
|
|
271
|
+
active = self._is_active()
|
|
272
|
+
raw, content = self._consume_line_comment(ctx)
|
|
273
|
+
self._emit_text(output, raw, ctx.file_name, start_line, col=start_col, active=active)
|
|
274
|
+
if active:
|
|
275
|
+
self._record_comment('slashes', content, ctx.file_name, start_line, start_col, ctx)
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if ch == '{':
|
|
279
|
+
start_line = ctx.line
|
|
280
|
+
start_col = ctx.col
|
|
281
|
+
active = self._is_active()
|
|
282
|
+
if ctx.peek(1) == '$':
|
|
283
|
+
raw_text, content = self._consume_brace_directive(ctx)
|
|
284
|
+
self._handle_directive(
|
|
285
|
+
raw_text,
|
|
286
|
+
content,
|
|
287
|
+
ctx,
|
|
288
|
+
start_line,
|
|
289
|
+
start_col,
|
|
290
|
+
contexts,
|
|
291
|
+
include_stack,
|
|
292
|
+
output,
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
raw, content = self._consume_brace_comment(ctx)
|
|
296
|
+
self._emit_text(
|
|
297
|
+
output,
|
|
298
|
+
raw,
|
|
299
|
+
ctx.file_name,
|
|
300
|
+
start_line,
|
|
301
|
+
col=start_col,
|
|
302
|
+
active=active,
|
|
303
|
+
)
|
|
304
|
+
if active:
|
|
305
|
+
self._record_comment('borland', content, ctx.file_name, start_line, start_col, ctx)
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
if ch == '(' and ctx.peek(1) == '*':
|
|
309
|
+
start_line = ctx.line
|
|
310
|
+
start_col = ctx.col
|
|
311
|
+
active = self._is_active()
|
|
312
|
+
if ctx.peek(2) == '$':
|
|
313
|
+
raw_text, content = self._consume_paren_directive(ctx)
|
|
314
|
+
self._handle_directive(
|
|
315
|
+
raw_text,
|
|
316
|
+
content,
|
|
317
|
+
ctx,
|
|
318
|
+
start_line,
|
|
319
|
+
start_col,
|
|
320
|
+
contexts,
|
|
321
|
+
include_stack,
|
|
322
|
+
output,
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
raw, content = self._consume_paren_comment(ctx)
|
|
326
|
+
self._emit_text(
|
|
327
|
+
output,
|
|
328
|
+
raw,
|
|
329
|
+
ctx.file_name,
|
|
330
|
+
start_line,
|
|
331
|
+
col=start_col,
|
|
332
|
+
active=active,
|
|
333
|
+
)
|
|
334
|
+
if active:
|
|
335
|
+
self._record_comment('ansi', content, ctx.file_name, start_line, start_col, ctx)
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
start_line = ctx.line
|
|
339
|
+
start_col = ctx.col
|
|
340
|
+
ch = ctx.advance(1)
|
|
341
|
+
self._emit_text(
|
|
342
|
+
output,
|
|
343
|
+
ch,
|
|
344
|
+
ctx.file_name,
|
|
345
|
+
start_line,
|
|
346
|
+
col=start_col,
|
|
347
|
+
active=self._is_active(),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return PreprocessedSource(
|
|
351
|
+
text=output.text,
|
|
352
|
+
source_map=output.line_map,
|
|
353
|
+
defines=self.defines,
|
|
354
|
+
scoped_enums=self.scoped_enums,
|
|
355
|
+
problems=self._problems,
|
|
356
|
+
comments=self._comments,
|
|
357
|
+
included_files=tuple(self._included_files),
|
|
358
|
+
source_segments=output.line_segments,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _apply_default_compiler_defines(self) -> None:
|
|
362
|
+
if not self.options.use_defines:
|
|
363
|
+
return
|
|
364
|
+
if 'FPC' in self.defines:
|
|
365
|
+
return
|
|
366
|
+
if self.options.compiler_version >= 20.0:
|
|
367
|
+
self.defines.add('CONDITIONALEXPRESSIONS')
|
|
368
|
+
self.defines.add('UNICODE')
|
|
369
|
+
version_define = int(round(self.options.compiler_version * 10))
|
|
370
|
+
self.defines.add(f'VER{version_define}')
|
|
371
|
+
|
|
372
|
+
def _emit_text(
|
|
373
|
+
self,
|
|
374
|
+
output: _OutputBuffer,
|
|
375
|
+
text: str,
|
|
376
|
+
file_name: str,
|
|
377
|
+
line: int,
|
|
378
|
+
*,
|
|
379
|
+
col: int = 1,
|
|
380
|
+
active: bool,
|
|
381
|
+
) -> None:
|
|
382
|
+
current_line = line
|
|
383
|
+
current_col = col
|
|
384
|
+
for ch in text:
|
|
385
|
+
if ch == '\n':
|
|
386
|
+
output.append_char('\n', file_name, current_line, current_col - 1)
|
|
387
|
+
current_line += 1
|
|
388
|
+
current_col = 1
|
|
389
|
+
else:
|
|
390
|
+
if active:
|
|
391
|
+
output.append_char(ch, file_name, current_line, current_col - 1)
|
|
392
|
+
else:
|
|
393
|
+
output.append_char(' ', file_name, current_line, current_col - 1)
|
|
394
|
+
current_col += 1
|
|
395
|
+
|
|
396
|
+
def _is_active(self) -> bool:
|
|
397
|
+
if not self._conditional_stack:
|
|
398
|
+
return True
|
|
399
|
+
return self._conditional_stack[-1].current_active
|
|
400
|
+
|
|
401
|
+
def _handle_directive(
|
|
402
|
+
self,
|
|
403
|
+
directive_raw: str,
|
|
404
|
+
directive_content: str,
|
|
405
|
+
ctx: _FileContext,
|
|
406
|
+
start_line: int,
|
|
407
|
+
start_col: int,
|
|
408
|
+
contexts: list[_FileContext],
|
|
409
|
+
include_stack: list[str],
|
|
410
|
+
output: _OutputBuffer,
|
|
411
|
+
) -> None:
|
|
412
|
+
name, param = self._parse_directive(directive_content)
|
|
413
|
+
replacement = self._replace_with_spaces(directive_raw)
|
|
414
|
+
|
|
415
|
+
if not name:
|
|
416
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
if name in {'IFDEF', 'IFNDEF', 'IF', 'IFOPT'}:
|
|
420
|
+
condition = False
|
|
421
|
+
if self._is_active():
|
|
422
|
+
if name == 'IFDEF':
|
|
423
|
+
condition = self._is_defined(param)
|
|
424
|
+
elif name == 'IFNDEF':
|
|
425
|
+
condition = not self._is_defined(param)
|
|
426
|
+
elif name == 'IF':
|
|
427
|
+
condition = self._evaluate_conditional_expression(param)
|
|
428
|
+
elif name == 'IFOPT':
|
|
429
|
+
condition = self._evaluate_ifopt(param)
|
|
430
|
+
frame = _ConditionalFrame(
|
|
431
|
+
parent_active=self._is_active(),
|
|
432
|
+
branch_taken=condition,
|
|
433
|
+
current_active=self._is_active() and condition,
|
|
434
|
+
)
|
|
435
|
+
self._conditional_stack.append(frame)
|
|
436
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
if name in {'ELSEIF', 'ELSE', 'ENDIF', 'IFEND'}:
|
|
440
|
+
if not self._conditional_stack:
|
|
441
|
+
self._problems.append(
|
|
442
|
+
PreprocessorProblem(
|
|
443
|
+
kind='directive',
|
|
444
|
+
message=f'unexpected {name}',
|
|
445
|
+
file_name=ctx.file_name,
|
|
446
|
+
line=start_line,
|
|
447
|
+
col=start_col,
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
frame = self._conditional_stack[-1]
|
|
454
|
+
if name == 'ELSEIF':
|
|
455
|
+
if not frame.parent_active:
|
|
456
|
+
frame.current_active = False
|
|
457
|
+
elif frame.branch_taken:
|
|
458
|
+
frame.current_active = False
|
|
459
|
+
else:
|
|
460
|
+
condition = self._evaluate_conditional_expression(param)
|
|
461
|
+
frame.current_active = frame.parent_active and condition
|
|
462
|
+
if condition:
|
|
463
|
+
frame.branch_taken = True
|
|
464
|
+
elif name == 'ELSE':
|
|
465
|
+
if not frame.parent_active:
|
|
466
|
+
frame.current_active = False
|
|
467
|
+
else:
|
|
468
|
+
frame.current_active = frame.parent_active and not frame.branch_taken
|
|
469
|
+
frame.branch_taken = True
|
|
470
|
+
else:
|
|
471
|
+
self._conditional_stack.pop()
|
|
472
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
if name in {'DEFINE', 'UNDEF'}:
|
|
476
|
+
if self._is_active() and self.options.use_defines:
|
|
477
|
+
for define_name in self._parse_define_list(param):
|
|
478
|
+
if name == 'DEFINE':
|
|
479
|
+
self.defines.add(define_name)
|
|
480
|
+
else:
|
|
481
|
+
self.defines.discard(define_name)
|
|
482
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
if name in {'SCOPEDENUMS'}:
|
|
486
|
+
if self._is_active():
|
|
487
|
+
self.scoped_enums = self._parse_on_off(param)
|
|
488
|
+
self._set_option('SCOPEDENUMS', 'ON' if self.scoped_enums else 'OFF')
|
|
489
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
if name in {'PUSHOPT', 'POPOPT'}:
|
|
493
|
+
if self._is_active():
|
|
494
|
+
if name == 'PUSHOPT':
|
|
495
|
+
self._option_stack.append((dict(self._option_values), self.scoped_enums))
|
|
496
|
+
else:
|
|
497
|
+
if self._option_stack:
|
|
498
|
+
self._option_values, self.scoped_enums = self._option_stack.pop()
|
|
499
|
+
else:
|
|
500
|
+
self._problems.append(
|
|
501
|
+
PreprocessorProblem(
|
|
502
|
+
kind='directive',
|
|
503
|
+
message='POPOPT without matching PUSHOPT',
|
|
504
|
+
file_name=ctx.file_name,
|
|
505
|
+
line=start_line,
|
|
506
|
+
col=start_col,
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
if name == 'OPT':
|
|
513
|
+
if self._is_active():
|
|
514
|
+
self._apply_opt_directive(param)
|
|
515
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
if name in {'I', 'INCLUDE'}:
|
|
519
|
+
if self._is_active():
|
|
520
|
+
include_name = self._extract_include_name(param)
|
|
521
|
+
if include_name:
|
|
522
|
+
self._last_include_error = ""
|
|
523
|
+
resolved = self.include_loader(ctx.file_name, include_name)
|
|
524
|
+
if resolved is not None and not self._include_result_is_allowed(resolved[1]):
|
|
525
|
+
self._last_include_error = f"include resolves outside workspace: {include_name}"
|
|
526
|
+
resolved = None
|
|
527
|
+
if resolved is None:
|
|
528
|
+
self._problems.append(
|
|
529
|
+
PreprocessorProblem(
|
|
530
|
+
kind='include',
|
|
531
|
+
message=self._last_include_error or f'include not found: {include_name}',
|
|
532
|
+
file_name=ctx.file_name,
|
|
533
|
+
line=start_line,
|
|
534
|
+
col=start_col,
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
|
+
content, resolved_path = resolved
|
|
539
|
+
if resolved_path in include_stack:
|
|
540
|
+
self._problems.append(
|
|
541
|
+
PreprocessorProblem(
|
|
542
|
+
kind='include',
|
|
543
|
+
message=f'include cycle detected: {resolved_path}',
|
|
544
|
+
file_name=ctx.file_name,
|
|
545
|
+
line=start_line,
|
|
546
|
+
col=start_col,
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
else:
|
|
550
|
+
if resolved_path not in self._included_files:
|
|
551
|
+
self._included_files.append(resolved_path)
|
|
552
|
+
include_stack.append(resolved_path)
|
|
553
|
+
ctx.pending_output = replacement
|
|
554
|
+
ctx.pending_line = start_line
|
|
555
|
+
ctx.pending_col = start_col
|
|
556
|
+
normalized = self._normalize_newlines(content)
|
|
557
|
+
if not normalized.endswith('\n'):
|
|
558
|
+
normalized += '\n'
|
|
559
|
+
contexts.append(_FileContext(text=normalized, file_name=resolved_path))
|
|
560
|
+
return
|
|
561
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
if self._is_active() and self._apply_named_option(name, param):
|
|
565
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
self._emit_text(output, replacement, ctx.file_name, start_line, col=start_col, active=True)
|
|
569
|
+
|
|
570
|
+
def _parse_directive(self, directive_text: str) -> tuple[str, str]:
|
|
571
|
+
text = directive_text.strip()
|
|
572
|
+
if not text:
|
|
573
|
+
return ('', '')
|
|
574
|
+
name = []
|
|
575
|
+
idx = 0
|
|
576
|
+
while idx < len(text) and (text[idx].isalpha() or text[idx] == '_'):
|
|
577
|
+
name.append(text[idx])
|
|
578
|
+
idx += 1
|
|
579
|
+
if not name:
|
|
580
|
+
return ('', '')
|
|
581
|
+
param = text[idx:].strip()
|
|
582
|
+
return (''.join(name).upper(), param)
|
|
583
|
+
|
|
584
|
+
def _parse_define_list(self, param: str) -> list[str]:
|
|
585
|
+
if not param:
|
|
586
|
+
return []
|
|
587
|
+
cleaned = param.replace(',', ' ')
|
|
588
|
+
names = [self._normalize_define(part) for part in cleaned.split() if part]
|
|
589
|
+
return [n for n in names if n]
|
|
590
|
+
|
|
591
|
+
def _evaluate_ifopt(self, param: str) -> bool:
|
|
592
|
+
if not param:
|
|
593
|
+
return False
|
|
594
|
+
token = param.strip().upper()
|
|
595
|
+
if not token:
|
|
596
|
+
return False
|
|
597
|
+
if token.endswith('+') and len(token) > 1:
|
|
598
|
+
option_name = token[:-1].strip()
|
|
599
|
+
return self._get_option(option_name) == 'ON'
|
|
600
|
+
if token.endswith('-') and len(token) > 1:
|
|
601
|
+
option_name = token[:-1].strip()
|
|
602
|
+
return self._get_option(option_name) == 'OFF'
|
|
603
|
+
if '=' in token:
|
|
604
|
+
name, raw_state = token.split('=', 1)
|
|
605
|
+
expected = self._normalize_option_state(raw_state)
|
|
606
|
+
if not name.strip() or expected is None:
|
|
607
|
+
return False
|
|
608
|
+
return self._get_option(name) == expected
|
|
609
|
+
parts = token.split()
|
|
610
|
+
if len(parts) == 2:
|
|
611
|
+
expected = self._normalize_option_state(parts[1])
|
|
612
|
+
if expected is None:
|
|
613
|
+
return False
|
|
614
|
+
return self._get_option(parts[0]) == expected
|
|
615
|
+
return False
|
|
616
|
+
|
|
617
|
+
def _evaluate_conditional_expression(self, param: str) -> bool:
|
|
618
|
+
text = param.strip().upper()
|
|
619
|
+
if text.startswith('COMPILERVERSION'):
|
|
620
|
+
return self._evaluate_version(text, 'COMPILERVERSION', self.options.compiler_version)
|
|
621
|
+
if text.startswith('RTLVERSION'):
|
|
622
|
+
return self._evaluate_version(text, 'RTLVERSION', self.options.rtl_version)
|
|
623
|
+
if text.startswith('DEFINED(') or text.startswith('NOT DEFINED('):
|
|
624
|
+
return self._evaluate_defined_chain(text)
|
|
625
|
+
return False
|
|
626
|
+
|
|
627
|
+
def _evaluate_version(self, text: str, label: str, value: float) -> bool:
|
|
628
|
+
rest = text[len(label):].strip()
|
|
629
|
+
if not rest:
|
|
630
|
+
return False
|
|
631
|
+
parts = rest.split()
|
|
632
|
+
if len(parts) < 2:
|
|
633
|
+
return False
|
|
634
|
+
oper = parts[0]
|
|
635
|
+
num = parts[1]
|
|
636
|
+
try:
|
|
637
|
+
right = float(num)
|
|
638
|
+
except ValueError:
|
|
639
|
+
return False
|
|
640
|
+
if oper == '=':
|
|
641
|
+
return value == right
|
|
642
|
+
if oper == '<>':
|
|
643
|
+
return value != right
|
|
644
|
+
if oper == '<':
|
|
645
|
+
return value < right
|
|
646
|
+
if oper == '<=':
|
|
647
|
+
return value <= right
|
|
648
|
+
if oper == '>':
|
|
649
|
+
return value > right
|
|
650
|
+
if oper == '>=':
|
|
651
|
+
return value >= right
|
|
652
|
+
return False
|
|
653
|
+
|
|
654
|
+
def _evaluate_defined_chain(self, text: str) -> bool:
|
|
655
|
+
remaining = text
|
|
656
|
+
result = True
|
|
657
|
+
evaluation = None
|
|
658
|
+
while remaining.startswith('DEFINED(') or remaining.startswith('NOT DEFINED('):
|
|
659
|
+
if remaining.startswith('DEFINED('):
|
|
660
|
+
define_name, remaining = self._consume_defined(remaining, 'DEFINED(')
|
|
661
|
+
cond = self._is_defined(define_name)
|
|
662
|
+
else:
|
|
663
|
+
define_name, remaining = self._consume_defined(remaining, 'NOT DEFINED(')
|
|
664
|
+
cond = not self._is_defined(define_name)
|
|
665
|
+
if evaluation is None:
|
|
666
|
+
result = cond
|
|
667
|
+
elif evaluation == 'AND':
|
|
668
|
+
result = result and cond
|
|
669
|
+
elif evaluation == 'OR':
|
|
670
|
+
result = result or cond
|
|
671
|
+
remaining = remaining.lstrip()
|
|
672
|
+
if remaining.startswith('AND '):
|
|
673
|
+
evaluation = 'AND'
|
|
674
|
+
remaining = remaining[4:]
|
|
675
|
+
elif remaining.startswith('OR '):
|
|
676
|
+
evaluation = 'OR'
|
|
677
|
+
remaining = remaining[3:]
|
|
678
|
+
return result
|
|
679
|
+
|
|
680
|
+
def _consume_defined(self, text: str, prefix: str) -> tuple[str, str]:
|
|
681
|
+
rest = text[len(prefix):]
|
|
682
|
+
end = rest.find(')')
|
|
683
|
+
if end == -1:
|
|
684
|
+
return ('', '')
|
|
685
|
+
name = rest[:end].strip()
|
|
686
|
+
remaining = rest[end + 1:]
|
|
687
|
+
return (name, remaining)
|
|
688
|
+
|
|
689
|
+
def _parse_on_off(self, param: str) -> bool:
|
|
690
|
+
token = param.strip().upper()
|
|
691
|
+
if token in {'ON', '1', 'TRUE', '+'}:
|
|
692
|
+
return True
|
|
693
|
+
if token in {'OFF', '0', 'FALSE', '-'}:
|
|
694
|
+
return False
|
|
695
|
+
return self.scoped_enums
|
|
696
|
+
|
|
697
|
+
def _is_defined(self, name: str) -> bool:
|
|
698
|
+
return self._normalize_define(name) in self.defines
|
|
699
|
+
|
|
700
|
+
def _normalize_define(self, name: str) -> str:
|
|
701
|
+
return name.strip().upper()
|
|
702
|
+
|
|
703
|
+
def _normalize_option_name(self, name: str) -> str:
|
|
704
|
+
return name.strip().upper()
|
|
705
|
+
|
|
706
|
+
def _normalize_option_state(self, raw_state: str) -> Optional[str]:
|
|
707
|
+
token = raw_state.strip().upper()
|
|
708
|
+
if token in {'+', 'ON', '1', 'TRUE'}:
|
|
709
|
+
return 'ON'
|
|
710
|
+
if token in {'-', 'OFF', '0', 'FALSE'}:
|
|
711
|
+
return 'OFF'
|
|
712
|
+
if token == 'AUTO':
|
|
713
|
+
return 'AUTO'
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
def _set_option(self, option_name: str, state: str) -> None:
|
|
717
|
+
name = self._normalize_option_name(option_name)
|
|
718
|
+
normalized = self._normalize_option_state(state)
|
|
719
|
+
if not name or normalized is None:
|
|
720
|
+
return
|
|
721
|
+
self._option_values[name] = normalized
|
|
722
|
+
if name == 'SCOPEDENUMS':
|
|
723
|
+
self.scoped_enums = normalized == 'ON'
|
|
724
|
+
|
|
725
|
+
def _get_option(self, option_name: str) -> str:
|
|
726
|
+
return self._option_values.get(self._normalize_option_name(option_name), '')
|
|
727
|
+
|
|
728
|
+
def _apply_named_option(self, name: str, param: str) -> bool:
|
|
729
|
+
if not name:
|
|
730
|
+
return False
|
|
731
|
+
cleaned = param.strip()
|
|
732
|
+
if not cleaned:
|
|
733
|
+
return False
|
|
734
|
+
if cleaned.startswith('='):
|
|
735
|
+
cleaned = cleaned[1:].strip()
|
|
736
|
+
state = self._normalize_option_state(cleaned)
|
|
737
|
+
if state is not None:
|
|
738
|
+
self._set_option(name, state)
|
|
739
|
+
return True
|
|
740
|
+
if len(cleaned) >= 2 and cleaned[0].isalpha() and cleaned[1] in {'+', '-'} and len(cleaned) == 2:
|
|
741
|
+
# Handles forms like {$OPT R+} where R+ is passed as the param token.
|
|
742
|
+
self._set_option(cleaned[0], cleaned[1])
|
|
743
|
+
return True
|
|
744
|
+
return False
|
|
745
|
+
|
|
746
|
+
def _apply_opt_directive(self, param: str) -> None:
|
|
747
|
+
if not param:
|
|
748
|
+
return
|
|
749
|
+
parts = [part for part in param.replace(',', ' ').split() if part]
|
|
750
|
+
index = 0
|
|
751
|
+
while index < len(parts):
|
|
752
|
+
part = parts[index]
|
|
753
|
+
if len(part) >= 2 and part[-1] in {'+', '-'}:
|
|
754
|
+
self._set_option(part[:-1], part[-1])
|
|
755
|
+
index += 1
|
|
756
|
+
continue
|
|
757
|
+
if '=' in part:
|
|
758
|
+
name, raw_state = part.split('=', 1)
|
|
759
|
+
self._set_option(name, raw_state)
|
|
760
|
+
index += 1
|
|
761
|
+
continue
|
|
762
|
+
if index + 1 < len(parts):
|
|
763
|
+
candidate_state = self._normalize_option_state(parts[index + 1])
|
|
764
|
+
if candidate_state is not None:
|
|
765
|
+
self._set_option(part, candidate_state)
|
|
766
|
+
index += 2
|
|
767
|
+
continue
|
|
768
|
+
index += 1
|
|
769
|
+
|
|
770
|
+
def _consume_line_comment(self, ctx: _FileContext) -> tuple[str, str]:
|
|
771
|
+
start = ctx.index
|
|
772
|
+
while not ctx.eof() and ctx.peek() not in {'\n'}:
|
|
773
|
+
ctx.advance(1)
|
|
774
|
+
raw = ctx.text[start:ctx.index]
|
|
775
|
+
return raw, raw[2:] if raw.startswith('//') else raw
|
|
776
|
+
|
|
777
|
+
def _consume_brace_comment(self, ctx: _FileContext) -> tuple[str, str]:
|
|
778
|
+
start = ctx.index
|
|
779
|
+
ctx.advance(1)
|
|
780
|
+
while not ctx.eof():
|
|
781
|
+
if ctx.peek() == '}':
|
|
782
|
+
ctx.advance(1)
|
|
783
|
+
break
|
|
784
|
+
ctx.advance(1)
|
|
785
|
+
else:
|
|
786
|
+
self._problems.append(
|
|
787
|
+
PreprocessorProblem(
|
|
788
|
+
kind='comment',
|
|
789
|
+
message='unterminated comment',
|
|
790
|
+
file_name=ctx.file_name,
|
|
791
|
+
line=ctx.line,
|
|
792
|
+
col=ctx.col,
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
raw = ctx.text[start:ctx.index]
|
|
796
|
+
if raw.endswith('}'):
|
|
797
|
+
return raw, raw[1:-1]
|
|
798
|
+
return raw, raw[1:]
|
|
799
|
+
|
|
800
|
+
def _consume_paren_comment(self, ctx: _FileContext) -> tuple[str, str]:
|
|
801
|
+
start = ctx.index
|
|
802
|
+
ctx.advance(2)
|
|
803
|
+
while not ctx.eof():
|
|
804
|
+
if ctx.peek() == '*' and ctx.peek(1) == ')':
|
|
805
|
+
ctx.advance(2)
|
|
806
|
+
break
|
|
807
|
+
ctx.advance(1)
|
|
808
|
+
else:
|
|
809
|
+
self._problems.append(
|
|
810
|
+
PreprocessorProblem(
|
|
811
|
+
kind='comment',
|
|
812
|
+
message='unterminated comment',
|
|
813
|
+
file_name=ctx.file_name,
|
|
814
|
+
line=ctx.line,
|
|
815
|
+
col=ctx.col,
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
raw = ctx.text[start:ctx.index]
|
|
819
|
+
if raw.endswith('*)'):
|
|
820
|
+
return raw, raw[2:-2]
|
|
821
|
+
return raw, raw[2:]
|
|
822
|
+
|
|
823
|
+
def _consume_brace_directive(self, ctx: _FileContext) -> tuple[str, str]:
|
|
824
|
+
start = ctx.index
|
|
825
|
+
ctx.advance(2)
|
|
826
|
+
while not ctx.eof():
|
|
827
|
+
if ctx.peek() == '}':
|
|
828
|
+
ctx.advance(1)
|
|
829
|
+
break
|
|
830
|
+
ctx.advance(1)
|
|
831
|
+
else:
|
|
832
|
+
self._problems.append(
|
|
833
|
+
PreprocessorProblem(
|
|
834
|
+
kind='directive',
|
|
835
|
+
message='unterminated directive',
|
|
836
|
+
file_name=ctx.file_name,
|
|
837
|
+
line=ctx.line,
|
|
838
|
+
col=ctx.col,
|
|
839
|
+
)
|
|
840
|
+
)
|
|
841
|
+
token = ctx.text[start:ctx.index]
|
|
842
|
+
content = token[2:-1].strip() if token.endswith('}') else token[2:].strip()
|
|
843
|
+
return token, content
|
|
844
|
+
|
|
845
|
+
def _consume_paren_directive(self, ctx: _FileContext) -> tuple[str, str]:
|
|
846
|
+
start = ctx.index
|
|
847
|
+
ctx.advance(3)
|
|
848
|
+
while not ctx.eof():
|
|
849
|
+
if ctx.peek() == '*' and ctx.peek(1) == ')':
|
|
850
|
+
ctx.advance(2)
|
|
851
|
+
break
|
|
852
|
+
ctx.advance(1)
|
|
853
|
+
else:
|
|
854
|
+
self._problems.append(
|
|
855
|
+
PreprocessorProblem(
|
|
856
|
+
kind='directive',
|
|
857
|
+
message='unterminated directive',
|
|
858
|
+
file_name=ctx.file_name,
|
|
859
|
+
line=ctx.line,
|
|
860
|
+
col=ctx.col,
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
token = ctx.text[start:ctx.index]
|
|
864
|
+
if token.endswith('*)'):
|
|
865
|
+
content = token[3:-2].strip()
|
|
866
|
+
else:
|
|
867
|
+
content = token[3:].strip()
|
|
868
|
+
return token, content
|
|
869
|
+
|
|
870
|
+
def _consume_string(self, ctx: _FileContext) -> str:
|
|
871
|
+
start = ctx.index
|
|
872
|
+
for quote_count in (5, 3):
|
|
873
|
+
delimiter = "'" * quote_count
|
|
874
|
+
if not ctx.text.startswith(delimiter, start):
|
|
875
|
+
continue
|
|
876
|
+
content_start = start + quote_count
|
|
877
|
+
if ctx.text.startswith('\r\n', content_start):
|
|
878
|
+
content_start += 2
|
|
879
|
+
elif content_start < len(ctx.text) and ctx.text[content_start] == '\n':
|
|
880
|
+
content_start += 1
|
|
881
|
+
else:
|
|
882
|
+
continue
|
|
883
|
+
close = ctx.text.find(delimiter, content_start)
|
|
884
|
+
if close < 0:
|
|
885
|
+
self._problems.append(
|
|
886
|
+
PreprocessorProblem(
|
|
887
|
+
kind='string',
|
|
888
|
+
message='unterminated multiline string literal',
|
|
889
|
+
file_name=ctx.file_name,
|
|
890
|
+
line=ctx.line,
|
|
891
|
+
col=ctx.col,
|
|
892
|
+
)
|
|
893
|
+
)
|
|
894
|
+
ctx.advance(len(ctx.text) - start)
|
|
895
|
+
else:
|
|
896
|
+
ctx.advance(close + quote_count - start)
|
|
897
|
+
return ctx.text[start:ctx.index]
|
|
898
|
+
ctx.advance(1)
|
|
899
|
+
while not ctx.eof():
|
|
900
|
+
ch = ctx.peek()
|
|
901
|
+
if ch == "'":
|
|
902
|
+
if ctx.peek(1) == "'":
|
|
903
|
+
ctx.advance(2)
|
|
904
|
+
continue
|
|
905
|
+
ctx.advance(1)
|
|
906
|
+
break
|
|
907
|
+
if ch == '\n':
|
|
908
|
+
self._problems.append(
|
|
909
|
+
PreprocessorProblem(
|
|
910
|
+
kind='string',
|
|
911
|
+
message='unterminated string literal',
|
|
912
|
+
file_name=ctx.file_name,
|
|
913
|
+
line=ctx.line,
|
|
914
|
+
col=ctx.col,
|
|
915
|
+
)
|
|
916
|
+
)
|
|
917
|
+
break
|
|
918
|
+
ctx.advance(1)
|
|
919
|
+
return ctx.text[start:ctx.index]
|
|
920
|
+
|
|
921
|
+
def _extract_include_name(self, param: str) -> str:
|
|
922
|
+
text = param.strip()
|
|
923
|
+
if not text:
|
|
924
|
+
return ''
|
|
925
|
+
if text[0] in {"'", '"'}:
|
|
926
|
+
quote = text[0]
|
|
927
|
+
end = text.find(quote, 1)
|
|
928
|
+
if end == -1:
|
|
929
|
+
return text[1:]
|
|
930
|
+
return text[1:end]
|
|
931
|
+
parts = text.split()
|
|
932
|
+
return parts[0] if parts else ''
|
|
933
|
+
|
|
934
|
+
def _replace_with_spaces(self, text: str) -> str:
|
|
935
|
+
return ''.join('\n' if ch == '\n' else ' ' for ch in text)
|
|
936
|
+
|
|
937
|
+
def _normalize_newlines(self, text: str) -> str:
|
|
938
|
+
return text.replace('\ufeff', '').replace('\r\n', '\n').replace('\r', '\n')
|
|
939
|
+
|
|
940
|
+
def _default_include_loader(self, parent_file: str, include_name: str) -> Optional[tuple[str, str]]:
|
|
941
|
+
workspace_root = self._active_workspace_root
|
|
942
|
+
parent_path = Path(parent_file).expanduser()
|
|
943
|
+
if not parent_path.is_absolute() and workspace_root is not None:
|
|
944
|
+
parent_path = workspace_root / parent_path
|
|
945
|
+
include_path = Path(include_name.replace('\\', '/'))
|
|
946
|
+
search_paths = [parent_path.parent]
|
|
947
|
+
for include_root in self.include_paths:
|
|
948
|
+
resolved_root = include_root.expanduser()
|
|
949
|
+
if not resolved_root.is_absolute() and workspace_root is not None:
|
|
950
|
+
resolved_root = workspace_root / resolved_root
|
|
951
|
+
search_paths.append(resolved_root)
|
|
952
|
+
for base in search_paths:
|
|
953
|
+
candidate = (include_path if include_path.is_absolute() else base / include_path).resolve()
|
|
954
|
+
if not self._path_is_within_workspace(candidate):
|
|
955
|
+
self._last_include_error = f'include resolves outside workspace: {include_name}'
|
|
956
|
+
continue
|
|
957
|
+
candidate_text = str(candidate)
|
|
958
|
+
if candidate_text not in self._included_files:
|
|
959
|
+
self._included_files.append(candidate_text)
|
|
960
|
+
if candidate.exists():
|
|
961
|
+
return (read_source_text(candidate), candidate_text)
|
|
962
|
+
return None
|
|
963
|
+
|
|
964
|
+
def _include_result_is_allowed(self, resolved_path: str) -> bool:
|
|
965
|
+
if not self._uses_default_include_loader and self.workspace_root is None:
|
|
966
|
+
return True
|
|
967
|
+
candidate = Path(resolved_path).expanduser()
|
|
968
|
+
if not candidate.is_absolute() and self._active_workspace_root is not None:
|
|
969
|
+
candidate = self._active_workspace_root / candidate
|
|
970
|
+
return self._path_is_within_workspace(candidate.resolve())
|
|
971
|
+
|
|
972
|
+
def _path_is_within_workspace(self, candidate: Path) -> bool:
|
|
973
|
+
root = self._active_workspace_root
|
|
974
|
+
return root is None or candidate == root or candidate.is_relative_to(root)
|
|
975
|
+
|
|
976
|
+
def _record_comment(
|
|
977
|
+
self,
|
|
978
|
+
kind: str,
|
|
979
|
+
text: str,
|
|
980
|
+
file_name: str,
|
|
981
|
+
start_line: int,
|
|
982
|
+
start_col: int,
|
|
983
|
+
ctx: _FileContext,
|
|
984
|
+
) -> None:
|
|
985
|
+
end_line = ctx.line
|
|
986
|
+
end_col = max(ctx.col - 1, 1)
|
|
987
|
+
self._comments.append(
|
|
988
|
+
CommentInfo(
|
|
989
|
+
kind=kind,
|
|
990
|
+
text=text,
|
|
991
|
+
file_name=file_name,
|
|
992
|
+
line=start_line,
|
|
993
|
+
col=start_col,
|
|
994
|
+
end_line=end_line,
|
|
995
|
+
end_col=end_col,
|
|
996
|
+
)
|
|
997
|
+
)
|