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.
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/PKG-INFO +4 -1
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/pyproject.toml +7 -1
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/src/dbrownell_ParserLib/__init__.py +15 -1
- dbrownell_parserlib-0.7.0/src/dbrownell_ParserLib/antlr/__init__.py +20 -0
- dbrownell_parserlib-0.7.0/src/dbrownell_ParserLib/antlr/antlr_parser.py +503 -0
- dbrownell_parserlib-0.7.0/src/dbrownell_ParserLib/antlr/antlr_visitor_mixins.py +155 -0
- dbrownell_parserlib-0.5.0/src/dbrownell_ParserLib/antlr/__init__.py +0 -8
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/README.md +0 -0
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/__init__.py +0 -0
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/src/dbrownell_ParserLib/antlr/build_antlr_grammar/antlr-4.13.2-complete.jar +0 -0
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/src/dbrownell_ParserLib/errors.py +0 -0
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/src/dbrownell_ParserLib/location.py +0 -0
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/src/dbrownell_ParserLib/py.typed +0 -0
- {dbrownell_parserlib-0.5.0 → dbrownell_parserlib-0.7.0}/src/dbrownell_ParserLib/region.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dbrownell-parserlib
|
|
3
|
-
Version: 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.
|
|
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
|
|
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
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|