dbrownell-parserlib 0.5.0__tar.gz → 0.7.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dbrownell-parserlib
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Functionality useful when creating parsers.
5
5
  Author: Dave Brownell
6
6
  Author-email: Dave Brownell <github@DavidBrownell.com>
@@ -10,7 +10,10 @@ Classifier: Operating System :: Microsoft :: Windows
10
10
  Classifier: Operating System :: POSIX :: Linux
11
11
  Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: antlr-denter>=1.3.1
14
+ Requires-Dist: antlr4-python3-runtime>=4.13.2
13
15
  Requires-Dist: dbrownell-common>=0.16.8
16
+ Requires-Dist: plum>=2.9.0
14
17
  Requires-Python: >=3.14
15
18
  Project-URL: Homepage, https://github.com/davidbrownell/dbrownell_ParserLib
16
19
  Project-URL: Documentation, https://github.com/davidbrownell/dbrownell_ParserLib
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbrownell-parserlib"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  # ^^^^^
5
5
  # Wheel names will be generated according to this value. Do not manually modify this value; instead
6
6
  # update it according to committed changes by running this command from the root of the repository:
@@ -14,7 +14,10 @@ authors = [
14
14
  ]
15
15
  requires-python = ">= 3.14"
16
16
  dependencies = [
17
+ "antlr-denter>=1.3.1",
18
+ "antlr4-python3-runtime>=4.13.2",
17
19
  "dbrownell-common>=0.16.8",
20
+ "plum>=2.9.0",
18
21
  ]
19
22
  classifiers = [
20
23
  "Operating System :: MacOS",
@@ -92,5 +95,8 @@ max-args = 10
92
95
  max-branches = 20
93
96
  max-returns = 20
94
97
 
98
+ [tool.ty.rules]
99
+ useless-overload-body = "ignore" # This rule is not compatible with plum overloads
100
+
95
101
  [tool.uv.build-backend]
96
102
  module-name = "dbrownell_ParserLib"
@@ -2,7 +2,15 @@
2
2
 
3
3
  from importlib.metadata import version
4
4
 
5
- from dbrownell_ParserLib.antlr import BuildAntlrGrammar
5
+ from dbrownell_ParserLib.antlr import (
6
+ AntlrParser,
7
+ AntlrParserException,
8
+ AntlrVisitorMixinBase,
9
+ BuildAntlrGrammar,
10
+ CreateAntlrParser,
11
+ InsignificantWhitespaceAntlrVisitorMixin,
12
+ SignificantWhitespaceAntlrVisitorMixin,
13
+ )
6
14
  from dbrownell_ParserLib.errors import Error, PythonError
7
15
  from dbrownell_ParserLib.location import Location
8
16
  from dbrownell_ParserLib.region import Region
@@ -10,11 +18,17 @@ from dbrownell_ParserLib.region import Region
10
18
 
11
19
  # ----------------------------------------------------------------------
12
20
  __all__ = [
21
+ "AntlrParser",
22
+ "AntlrParserException",
23
+ "AntlrVisitorMixinBase",
13
24
  "BuildAntlrGrammar",
25
+ "CreateAntlrParser",
14
26
  "Error",
27
+ "InsignificantWhitespaceAntlrVisitorMixin",
15
28
  "Location",
16
29
  "PythonError",
17
30
  "Region",
31
+ "SignificantWhitespaceAntlrVisitorMixin",
18
32
  "__version__",
19
33
  ]
20
34
 
@@ -0,0 +1,20 @@
1
+ # noqa: D104
2
+ from dbrownell_ParserLib.antlr.build_antlr_grammar import BuildAntlrGrammar
3
+ from dbrownell_ParserLib.antlr.antlr_parser import AntlrParser, AntlrParserException, CreateAntlrParser
4
+ from dbrownell_ParserLib.antlr.antlr_visitor_mixins import (
5
+ AntlrVisitorMixinBase,
6
+ InsignificantWhitespaceAntlrVisitorMixin,
7
+ SignificantWhitespaceAntlrVisitorMixin,
8
+ )
9
+
10
+
11
+ # ----------------------------------------------------------------------
12
+ __all__ = [
13
+ "AntlrParser",
14
+ "AntlrParserException",
15
+ "AntlrVisitorMixinBase",
16
+ "BuildAntlrGrammar",
17
+ "CreateAntlrParser",
18
+ "InsignificantWhitespaceAntlrVisitorMixin",
19
+ "SignificantWhitespaceAntlrVisitorMixin",
20
+ ]
@@ -0,0 +1,503 @@
1
+ """Contains functionality used when parsing with ANTLR parsers."""
2
+
3
+ import os
4
+ import threading
5
+
6
+ from abc import ABC, abstractmethod
7
+ from collections.abc import Callable # noqa: TC003
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime, UTC
10
+ from pathlib import Path
11
+ from typing import cast, Protocol
12
+
13
+ import antlr4
14
+
15
+ from dbrownell_Common.ContextlibEx import ExitStack
16
+ from dbrownell_Common import ExecuteTasks
17
+ from dbrownell_Common.Streams.DoneManager import DoneManager # noqa: TC002
18
+ from plum import dispatch, overload
19
+
20
+ from dbrownell_ParserLib.errors import Error, PythonError
21
+ from dbrownell_ParserLib.region import Location, Region
22
+
23
+
24
+ # ----------------------------------------------------------------------
25
+ # |
26
+ # | Public Types
27
+ # |
28
+ # ----------------------------------------------------------------------
29
+ class AntlrParserException(Exception): # noqa: N818
30
+ """Exception raised by the ANTLR parser or a visitor processing the AST."""
31
+
32
+ # ----------------------------------------------------------------------
33
+ def __init__(self, error: Error) -> None:
34
+ super().__init__(str(error))
35
+
36
+ self.error = error
37
+
38
+
39
+ # ----------------------------------------------------------------------
40
+ class AntlrParser(ABC):
41
+ """Abstract base class for parsers created with `CreateAntlrParser`."""
42
+
43
+ # ----------------------------------------------------------------------
44
+ @overload
45
+ @abstractmethod
46
+ def __call__(
47
+ self,
48
+ dm: DoneManager,
49
+ workspaces: dict[
50
+ Path, # workspace root
51
+ dict[
52
+ Path, # Relative path to a translation unit
53
+ Callable[
54
+ [], tuple[str, datetime]
55
+ ], # Function that returns the text and last modified time of the translation unit
56
+ ],
57
+ ],
58
+ supported_file_extensions: set[str] | None,
59
+ *,
60
+ single_threaded: bool = False,
61
+ quiet: bool = False,
62
+ ) -> dict[
63
+ Path, # workspace root
64
+ dict[
65
+ Path, # Relative path to a translation unit
66
+ Error | antlr4.ParseTreeVisitor,
67
+ ],
68
+ ]:
69
+ """Parse files from different workspaces; includes (if any) are allowed from each of the workspaces."""
70
+
71
+ # ----------------------------------------------------------------------
72
+ @overload
73
+ @abstractmethod
74
+ def __call__( # noqa: F811
75
+ self,
76
+ dm: DoneManager,
77
+ filenames: list[Path],
78
+ supported_file_extensions: set[str] | None,
79
+ *,
80
+ single_threaded: bool = False,
81
+ quiet: bool = False,
82
+ ) -> dict[Path, Error | antlr4.ParseTreeVisitor]:
83
+ """Parse a list of files that reside in the same directory and its descendants; includes (if any) are allowed from the same root directory."""
84
+
85
+ # ----------------------------------------------------------------------
86
+ @overload
87
+ @abstractmethod
88
+ def __call__( # noqa: F811
89
+ self,
90
+ dm: DoneManager,
91
+ filename: Path,
92
+ supported_file_extensions: set[str] | None,
93
+ *,
94
+ single_threaded: bool = False,
95
+ quiet: bool = False,
96
+ ) -> dict[Path, Error | antlr4.ParseTreeVisitor]:
97
+ """Parse a single file; includes (if any) are allowed from the same directory as the file."""
98
+
99
+ # ----------------------------------------------------------------------
100
+ @dispatch
101
+ @abstractmethod
102
+ def __call__(self, *args, **kwargs): # noqa: ANN204, F811
103
+ """Parse one or more translation units across one or more workspaces."""
104
+
105
+
106
+ # ----------------------------------------------------------------------
107
+ class CreateVisitorFuncType(Protocol):
108
+ """Signature for the function used to create the visitor for use with `CreateAntlrParser`."""
109
+
110
+ def __call__( # noqa: D102
111
+ self,
112
+ filename: Path,
113
+ on_progress_func: Callable[[int], None],
114
+ *,
115
+ is_included_file: bool,
116
+ ) -> antlr4.ParseTreeVisitor: ...
117
+
118
+
119
+ # ----------------------------------------------------------------------
120
+ # |
121
+ # | Public Functions
122
+ # |
123
+ # ----------------------------------------------------------------------
124
+ def CreateAntlrParser(
125
+ antlr_lexer: type[antlr4.Lexer],
126
+ antlr_parser: type[antlr4.Parser],
127
+ create_visitor_func: CreateVisitorFuncType,
128
+ get_ast_func: Callable[[antlr4.Parser], antlr4.ParserRuleContext],
129
+ custom_lexer_init_func: Callable[[antlr4.Lexer], None] | None = None,
130
+ custom_parser_init_func: Callable[[antlr4.Parser], None] | None = None,
131
+ ) -> AntlrParser:
132
+ """Create an `AntlrParser` that uses the given code generated by ANTLR."""
133
+
134
+ # ----------------------------------------------------------------------
135
+ def CreateLexer(input_stream: antlr4.InputStream) -> antlr4.Lexer:
136
+ lexer = antlr_lexer(input_stream)
137
+
138
+ if custom_lexer_init_func is not None:
139
+ custom_lexer_init_func(lexer)
140
+
141
+ return lexer
142
+
143
+ # ----------------------------------------------------------------------
144
+ def CreateParser(token_stream: antlr4.CommonTokenStream) -> antlr4.Parser:
145
+ parser = antlr_parser(token_stream)
146
+
147
+ if custom_parser_init_func is not None:
148
+ custom_parser_init_func(parser)
149
+
150
+ return parser
151
+
152
+ # ----------------------------------------------------------------------
153
+
154
+ return _AntlrParser(
155
+ CreateLexer,
156
+ CreateParser,
157
+ create_visitor_func,
158
+ get_ast_func,
159
+ )
160
+
161
+
162
+ # ----------------------------------------------------------------------
163
+ # |
164
+ # | Private Types
165
+ # |
166
+ # ----------------------------------------------------------------------
167
+ class _AntlrParser(AntlrParser):
168
+ # ----------------------------------------------------------------------
169
+ # | Public Methods
170
+ def __init__(
171
+ self,
172
+ create_lexer_func: Callable[[antlr4.InputStream], antlr4.Lexer],
173
+ create_parser_func: Callable[[antlr4.CommonTokenStream], antlr4.Parser],
174
+ create_visitor_func: CreateVisitorFuncType,
175
+ get_ast_func: Callable[[antlr4.Parser], antlr4.ParserRuleContext],
176
+ ) -> None:
177
+ self._create_lexer_func = create_lexer_func
178
+ self._create_parser_func = create_parser_func
179
+ self._create_visitor_func = create_visitor_func
180
+ self._get_ast_func = get_ast_func
181
+
182
+ self._execute_state: _AntlrParser._ExecuteState | None = None
183
+
184
+ # ----------------------------------------------------------------------
185
+ @overload
186
+ def __call__( # noqa: C901
187
+ self,
188
+ dm: DoneManager,
189
+ workspaces: dict[Path, dict[Path, Callable[[], tuple[str, datetime]]]],
190
+ supported_file_extensions: set[str] | None,
191
+ *,
192
+ single_threaded: bool = False,
193
+ quiet: bool = False,
194
+ ) -> dict[Path, dict[Path, Error | antlr4.ParseTreeVisitor]]:
195
+ _ = supported_file_extensions # Currently unused
196
+
197
+ # Create the workspace infos for all of the workspaces. At the same time, ensure that
198
+ # there aren't any roots that are nested within other roots.
199
+ all_roots: set[Path] = set()
200
+
201
+ # ----------------------------------------------------------------------
202
+ def CheckRoot(path: Path) -> None:
203
+ for root in all_roots:
204
+ if path == root:
205
+ msg = f"The path '{path.as_posix()}' is duplicated as a root."
206
+ raise ValueError(msg)
207
+
208
+ if path.is_relative_to(root) or root.is_relative_to(path):
209
+ if path.is_relative_to(root):
210
+ nested = path
211
+ container = root
212
+ elif root.is_relative_to(path):
213
+ nested = root
214
+ container = path
215
+ else:
216
+ assert False, (path, root) # noqa: B011, PT015 # pragma: no cover
217
+
218
+ msg = (
219
+ f"The path '{nested.as_posix()}' is nested within the root '{container.as_posix()}'."
220
+ )
221
+ raise ValueError(msg)
222
+
223
+ all_roots.add(path)
224
+
225
+ # ----------------------------------------------------------------------
226
+
227
+ workspace_infos: dict[Path, _AntlrParser._WorkspaceInfo] = {}
228
+
229
+ for workspace_name in workspaces:
230
+ workspace_path = workspace_name.resolve()
231
+
232
+ CheckRoot(workspace_path)
233
+ workspace_infos[workspace_path] = _AntlrParser._WorkspaceInfo()
234
+
235
+ # ----------------------------------------------------------------------
236
+ def OnExit() -> None:
237
+ self._execute_state = None
238
+
239
+ # ----------------------------------------------------------------------
240
+
241
+ with ExitStack(OnExit):
242
+ with ExecuteTasks.YieldQueueExecutor(
243
+ dm,
244
+ "Parsing...",
245
+ quiet=quiet,
246
+ max_num_threads=1 if single_threaded else None,
247
+ ) as enqueue_func:
248
+ self._execute_state = _AntlrParser._ExecuteState(enqueue_func, workspace_infos)
249
+
250
+ if len(workspace_infos) == 1:
251
+ create_description_func = lambda w, t: t # noqa: ARG005, E731
252
+ else:
253
+ create_description_func = lambda w, t: (w / t).as_posix() # noqa: E731
254
+
255
+ for (workspace_path, workspace_info), translation_units in zip(
256
+ workspace_infos.items(), workspaces.values(), strict=True
257
+ ):
258
+ with workspace_info.results_lock:
259
+ for translation_unit, content_func in translation_units.items():
260
+ # ----------------------------------------------------------------------
261
+ def ThisExecuteTask(
262
+ on_simple_status_func: Callable[[str], None], # noqa: ARG001
263
+ workspace_path: Path = workspace_path,
264
+ translation_unit: Path = translation_unit,
265
+ content_func: Callable[[], tuple[str, datetime]] = content_func,
266
+ ) -> tuple[int, ExecuteTasks.YieldQueueExecutorTypes.ExecuteFuncType]:
267
+ return self._ExecuteTask(
268
+ workspace_path,
269
+ translation_unit,
270
+ content_func,
271
+ is_included_file=False,
272
+ )
273
+
274
+ # ----------------------------------------------------------------------
275
+
276
+ workspace_info.results[translation_unit] = None
277
+
278
+ enqueue_func(
279
+ create_description_func(workspace_path, translation_unit),
280
+ ThisExecuteTask,
281
+ )
282
+
283
+ # Capture the results
284
+ results: dict[Path, dict[Path, Error | antlr4.ParseTreeVisitor]] = {}
285
+ errors: list[Error] = []
286
+
287
+ for workspace_path, workspace_info in workspace_infos.items():
288
+ these_results: dict[Path, Error | antlr4.ParseTreeVisitor] = {}
289
+
290
+ for translation_unit, translation_unit_result in workspace_info.results.items():
291
+ if translation_unit_result is None:
292
+ continue
293
+
294
+ these_results[translation_unit] = translation_unit_result
295
+
296
+ if isinstance(translation_unit_result, Error):
297
+ errors.append(translation_unit_result)
298
+
299
+ results[workspace_path] = these_results
300
+
301
+ dm.result = len(errors)
302
+
303
+ return results
304
+
305
+ # ----------------------------------------------------------------------
306
+ @overload
307
+ def __call__( # noqa: F811
308
+ self,
309
+ dm: DoneManager,
310
+ filenames: list[Path],
311
+ supported_file_extensions: set[str] | None,
312
+ *,
313
+ single_threaded: bool = False,
314
+ quiet: bool = False,
315
+ ) -> dict[Path, Error | antlr4.ParseTreeVisitor]:
316
+ if not filenames:
317
+ msg = "Invalid filenames"
318
+ raise ValueError(msg)
319
+
320
+ workspace_root: Path | None = None
321
+ translation_units: dict[Path, Callable[[], tuple[str, datetime]]] = {}
322
+
323
+ if len(filenames) == 1:
324
+ workspace_root = filenames[0].parent
325
+ translation_units[Path(filenames[0].name)] = lambda filename=filenames[0]: _LoadFile(filename)
326
+ else:
327
+ potential_root = os.path.commonpath(filenames)
328
+ if not potential_root:
329
+ msg = "Filenames must share a common root"
330
+ raise ValueError(msg)
331
+
332
+ workspace_root = Path(potential_root)
333
+
334
+ for filename in filenames:
335
+ translation_units[filename.relative_to(workspace_root)] = lambda filename=filename: _LoadFile(
336
+ filename
337
+ )
338
+
339
+ assert workspace_root is not None
340
+ assert translation_units
341
+
342
+ result = self(
343
+ dm,
344
+ {workspace_root: translation_units},
345
+ supported_file_extensions,
346
+ single_threaded=single_threaded,
347
+ quiet=quiet,
348
+ )
349
+
350
+ assert len(result) == 1
351
+ return next(iter(result.values()))
352
+
353
+ # ----------------------------------------------------------------------
354
+ @overload
355
+ def __call__( # noqa: F811
356
+ self,
357
+ dm: DoneManager,
358
+ filename: Path,
359
+ supported_file_extensions: set[str] | None,
360
+ *,
361
+ single_threaded: bool = False,
362
+ quiet: bool = False,
363
+ ) -> dict[Path, Error | antlr4.ParseTreeVisitor]:
364
+ return self(
365
+ dm,
366
+ [filename],
367
+ supported_file_extensions,
368
+ single_threaded=single_threaded,
369
+ quiet=quiet,
370
+ )
371
+
372
+ # ----------------------------------------------------------------------
373
+ @dispatch
374
+ def __call__(self, *args, **kwargs): # noqa: ANN204, F811
375
+ pass # pragma: no cover
376
+
377
+ # ----------------------------------------------------------------------
378
+ # | Private Types
379
+ @dataclass(frozen=True)
380
+ class _WorkspaceInfo:
381
+ results: dict[Path, None | Error | antlr4.ParseTreeVisitor] = field(init=False, default_factory=dict)
382
+ results_lock: threading.Lock = field(init=False, default_factory=threading.Lock)
383
+
384
+ # ----------------------------------------------------------------------
385
+ @dataclass(frozen=True)
386
+ class _ExecuteState:
387
+ enqueue_func: ExecuteTasks.YieldQueueExecutorTypes.EnqueueFuncType
388
+ workspace_infos: dict[Path, _AntlrParser._WorkspaceInfo]
389
+
390
+ # ----------------------------------------------------------------------
391
+ # | Private Methods
392
+ def _ExecuteTask(
393
+ self,
394
+ workspace_path: Path,
395
+ translation_unit: Path,
396
+ content_func: Callable[[], tuple[str, datetime]],
397
+ *,
398
+ is_included_file: bool,
399
+ ) -> tuple[int, ExecuteTasks.YieldQueueExecutorTypes.ExecuteFuncType]:
400
+ content, last_modified_time = content_func()
401
+
402
+ _ = last_modified_time # Currently unused
403
+
404
+ # ----------------------------------------------------------------------
405
+ def Execute(
406
+ status: ExecuteTasks.Status,
407
+ ) -> str | None:
408
+ result: None | Error | antlr4.ParseTreeVisitor = None
409
+
410
+ # ----------------------------------------------------------------------
411
+ def OnExit() -> None:
412
+ assert result is not None
413
+ assert self._execute_state is not None
414
+
415
+ workspace_info = self._execute_state.workspace_infos[workspace_path]
416
+
417
+ with workspace_info.results_lock:
418
+ assert workspace_info.results[translation_unit] is None
419
+ workspace_info.results[translation_unit] = result
420
+
421
+ # ----------------------------------------------------------------------
422
+
423
+ with ExitStack(OnExit):
424
+ try:
425
+ fullpath = workspace_path / translation_unit
426
+ error_listener = _ErrorListener(fullpath)
427
+
428
+ antlr_stream = antlr4.InputStream(content)
429
+
430
+ lexer = self._create_lexer_func(antlr_stream)
431
+
432
+ lexer.removeErrorListeners()
433
+ lexer.addErrorListener(error_listener)
434
+
435
+ tokens = antlr4.CommonTokenStream(lexer)
436
+ tokens.fill()
437
+
438
+ parser = self._create_parser_func(tokens)
439
+
440
+ parser.removeErrorListeners()
441
+ parser.addErrorListener(error_listener)
442
+
443
+ visitor = self._create_visitor_func(
444
+ fullpath,
445
+ lambda line: cast(None, status.OnProgress(line, None)),
446
+ is_included_file=is_included_file,
447
+ )
448
+
449
+ ast = self._get_ast_func(parser)
450
+ ast.accept(visitor)
451
+
452
+ result = visitor
453
+
454
+ except AntlrParserException as ex:
455
+ result = ex.error
456
+ except Exception as ex:
457
+ result = PythonError.Create(ex)
458
+
459
+ # ----------------------------------------------------------------------
460
+
461
+ return len(content.split("\n")), Execute
462
+
463
+
464
+ # ----------------------------------------------------------------------
465
+ class _ErrorListener(antlr4.DiagnosticErrorListener):
466
+ # ----------------------------------------------------------------------
467
+ def __init__(self, source: Path, *args, **kwargs) -> None:
468
+ super().__init__(*args, **kwargs)
469
+
470
+ self._source = source
471
+
472
+ # ----------------------------------------------------------------------
473
+ def syntaxError(
474
+ self,
475
+ recognizer: antlr4.Parser, # noqa: ARG002
476
+ offendingSymbol: antlr4.Token, # noqa: ARG002, N803
477
+ line: int,
478
+ column: int,
479
+ msg: str,
480
+ e: antlr4.RecognitionException | None,
481
+ ) -> None:
482
+ location = Location(line, column + 1)
483
+ error = Error(msg, Region(self._source, location, location))
484
+ ex = AntlrParserException(error)
485
+
486
+ if e is None:
487
+ raise ex
488
+
489
+ raise ex from e
490
+
491
+
492
+ # ----------------------------------------------------------------------
493
+ # |
494
+ # | Private Functions
495
+ # |
496
+ # ----------------------------------------------------------------------
497
+ def _LoadFile(filename: Path) -> tuple[str, datetime]:
498
+ """Load the contents of a file and return it along with its last modified time."""
499
+
500
+ text = filename.read_text(encoding="utf-8")
501
+ last_modified_time = datetime.fromtimestamp(filename.stat().st_mtime, tz=UTC)
502
+
503
+ return text, last_modified_time
@@ -0,0 +1,155 @@
1
+ """Various ANTLR visitor mixins that can be used to add functionality to the generated visitor classes."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import cast, TYPE_CHECKING
5
+
6
+ import antlr4
7
+
8
+ from dbrownell_ParserLib.region import Location, Region
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+
14
+
15
+ # ----------------------------------------------------------------------
16
+ class AntlrVisitorMixinBase(ABC):
17
+ """Base class for visitor mixins. This class is not meant to be used directly, but rather to be inherited by other mixin classes."""
18
+
19
+ # ----------------------------------------------------------------------
20
+ def __init__(
21
+ self,
22
+ filename: Path,
23
+ on_progress_func: Callable[[int], None],
24
+ *,
25
+ is_included_file: bool,
26
+ ) -> None:
27
+ self.filename = filename
28
+ self.is_included_file = is_included_file
29
+
30
+ # Protected data
31
+ self._on_progress_func = on_progress_func
32
+ self._stack: list[object] = []
33
+
34
+ # Private data
35
+ self._current_line: int = 0
36
+
37
+ # ----------------------------------------------------------------------
38
+ @abstractmethod
39
+ def CreateRegion(
40
+ self,
41
+ ctx: antlr4.ParserRuleContext,
42
+ ) -> Region:
43
+ """Create a `Region` from the given context."""
44
+
45
+ raise NotImplementedError() # pragma: no cover
46
+
47
+ # ----------------------------------------------------------------------
48
+ def GetChildren(self, ctx: antlr4.ParserRuleContext) -> list[object]:
49
+ """Return the results of visiting the children of the given context."""
50
+
51
+ prev_num_stack_items = len(self._stack)
52
+
53
+ cast(antlr4.ParseTreeVisitor, self).visitChildren(ctx)
54
+
55
+ results = self._stack[prev_num_stack_items:]
56
+ del self._stack[prev_num_stack_items:]
57
+
58
+ return results
59
+
60
+ # ----------------------------------------------------------------------
61
+ # | Protected Methods
62
+ def _OnProgress(self, end_line: int) -> None:
63
+ if end_line > self._current_line:
64
+ self._current_line = end_line
65
+ self._on_progress_func(end_line)
66
+
67
+
68
+ # ----------------------------------------------------------------------
69
+ class InsignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
70
+ """Mixin that adds functionality used for grammars that ignore insignificant whitespace."""
71
+
72
+ # ----------------------------------------------------------------------
73
+ def CreateRegion(
74
+ self,
75
+ ctx: antlr4.ParserRuleContext,
76
+ ) -> Region:
77
+ """Create a `Region` from the given context."""
78
+
79
+ start_token = ctx.start
80
+ stop_token = ctx.stop
81
+
82
+ if start_token is None or stop_token is None:
83
+ msg = "Context does not have start or stop token"
84
+ raise ValueError(msg)
85
+
86
+ return Region(
87
+ self.filename,
88
+ Location(start_token.line, start_token.column),
89
+ Location(stop_token.line, stop_token.column + len(stop_token.text)),
90
+ )
91
+
92
+
93
+ # ----------------------------------------------------------------------
94
+ class SignificantWhitespaceAntlrVisitorMixin(AntlrVisitorMixinBase):
95
+ """Mixin that adds functionality used for grammars that treat whitespace as significant."""
96
+
97
+ # ----------------------------------------------------------------------
98
+ def __init__(
99
+ self,
100
+ dedent_token: int,
101
+ newline_token: int,
102
+ newline_token_string: str,
103
+ ) -> None:
104
+ self._dedent_token = dedent_token
105
+ self._newline_token = newline_token
106
+ self._newline_token_string = newline_token_string
107
+
108
+ # ----------------------------------------------------------------------
109
+ def CreateRegion( # noqa: D102
110
+ self,
111
+ ctx: antlr4.ParserRuleContext,
112
+ ) -> Region:
113
+ assert isinstance(ctx.start, antlr4.Token), ctx.start
114
+ assert isinstance(ctx.stop, antlr4.Token), ctx.stop
115
+
116
+ if ctx.stop.type == self._dedent_token:
117
+ stop_line = ctx.stop.line
118
+ stop_col = ctx.stop.column
119
+
120
+ elif ctx.stop.type == self._newline_token and ctx.stop.text == self._newline_token_string:
121
+ if ctx.stop.line == ctx.start.line:
122
+ # This is the scenario where the statement is followed by a dedent followed by another
123
+ # statement. We don't want the range of this item to overlap with the range of the
124
+ # next item, so use the values as they are, even though it means that a statement
125
+ # that terminates with a newline will not have the newline here.
126
+ stop_line = ctx.stop.line
127
+ stop_col = ctx.stop.column
128
+ else:
129
+ stop_line = ctx.stop.line
130
+ stop_col = ctx.stop.column if ctx.stop.column == 0 else ctx.start.column
131
+
132
+ else:
133
+ stop_line = ctx.stop.line
134
+
135
+ content = ctx.stop.text
136
+ lines = content.split("\n")
137
+
138
+ if ctx.stop.type == self._newline_token:
139
+ assert content.startswith("\n"), content
140
+ assert len(lines) == 2, lines # noqa: PLR2004
141
+
142
+ stop_col = len(lines[-1])
143
+
144
+ if stop_line == ctx.stop.line:
145
+ stop_col += ctx.stop.column
146
+
147
+ stop_line += len(lines) - 1
148
+
149
+ self._OnProgress(stop_line)
150
+
151
+ return Region(
152
+ self.filename,
153
+ Location(ctx.start.line, ctx.start.column + 1),
154
+ Location(stop_line, stop_col),
155
+ )
@@ -1,8 +0,0 @@
1
- # noqa: D104
2
- from dbrownell_ParserLib.antlr.build_antlr_grammar import BuildAntlrGrammar
3
-
4
-
5
- # ----------------------------------------------------------------------
6
- __all__ = [
7
- "BuildAntlrGrammar",
8
- ]