lua-annotations 0.1.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.
- lua_annotations/__init__.py +0 -0
- lua_annotations/api/__init__.py +0 -0
- lua_annotations/api/annotations.py +123 -0
- lua_annotations/api/arguments.py +26 -0
- lua_annotations/api/lua_dict.py +192 -0
- lua_annotations/build_process.py +99 -0
- lua_annotations/exceptions.py +14 -0
- lua_annotations/extensions/__init__.py +0 -0
- lua_annotations/extensions/default.py +76 -0
- lua_annotations/extensions/game_framework/__init__.py +0 -0
- lua_annotations/extensions/game_framework/index.py +84 -0
- lua_annotations/extensions/game_framework/lifecycle.py +60 -0
- lua_annotations/extensions/game_framework/main.py +8 -0
- lua_annotations/extensions/game_framework/networking.py +65 -0
- lua_annotations/init_project.py +197 -0
- lua_annotations/main.py +59 -0
- lua_annotations/parser.py +277 -0
- lua_annotations/parser_schemas.py +139 -0
- lua_annotations/templates/AnnotationInit.lua +24 -0
- lua_annotations/templates/LazyLoadIndex.lua +23 -0
- lua_annotations/templates/annotations.config.json +13 -0
- lua_annotations-0.1.0.dist-info/METADATA +74 -0
- lua_annotations-0.1.0.dist-info/RECORD +27 -0
- lua_annotations-0.1.0.dist-info/WHEEL +5 -0
- lua_annotations-0.1.0.dist-info/entry_points.txt +2 -0
- lua_annotations-0.1.0.dist-info/licenses/LICENSE +201 -0
- lua_annotations-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from lua_annotations.api.annotations import ExtensionRegistry
|
|
2
|
+
from . import lifecycle, index, networking
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def load(ctx: ExtensionRegistry):
|
|
6
|
+
ctx.register_extension(index.IndexExtension())
|
|
7
|
+
ctx.register_extension(lifecycle.LifecycleExtension(), deps=['ManifestExtension'], hook_order='before')
|
|
8
|
+
ctx.register_extension(networking.NetworkingExtension())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from lua_annotations.api.annotations import AnnotationBuildCtx, AnnotationDef, ExtensionRegistry, Extension
|
|
4
|
+
from lua_annotations.build_process import PostProcessCtx
|
|
5
|
+
from lua_annotations.parser_schemas import Annotation, LuaMethod
|
|
6
|
+
|
|
7
|
+
REMOTE_INSTANCE_MAP = {
|
|
8
|
+
'function': 'RemoteFunction',
|
|
9
|
+
'event': 'RemoteEvent',
|
|
10
|
+
'unreliable': 'UnreliableRemoteEvent',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NetworkingExtension(Extension):
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.remotes: dict[Any, Any] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def remote_on_build(self, ctx: AnnotationBuildCtx):
|
|
20
|
+
anot: Annotation = ctx.annotation
|
|
21
|
+
adornee = anot.adornee
|
|
22
|
+
assert isinstance(adornee, LuaMethod)
|
|
23
|
+
|
|
24
|
+
class_name = REMOTE_INSTANCE_MAP[ctx.annotation.args_val[0]]
|
|
25
|
+
module_name = adornee.module.returned_name
|
|
26
|
+
|
|
27
|
+
anot.export_data["remote_name"] = adornee.name
|
|
28
|
+
anot.export_data["remote_parent"] = module_name
|
|
29
|
+
|
|
30
|
+
self.remotes.setdefault(module_name, {"ClassName": "Folder", "Children": {}})
|
|
31
|
+
self.remotes[module_name]["Children"][adornee.name] = {"ClassName": class_name}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def on_post_process(self, ctx: PostProcessCtx):
|
|
35
|
+
#Convert dict to valid .model.json format
|
|
36
|
+
root_children = []
|
|
37
|
+
|
|
38
|
+
for module_name, module_data in self.remotes.items():
|
|
39
|
+
module_children_map = module_data.get("Children", {})
|
|
40
|
+
module_children = []
|
|
41
|
+
|
|
42
|
+
for remote_name, remote_data in module_children_map.items():
|
|
43
|
+
module_children.append({
|
|
44
|
+
"Name": remote_name,
|
|
45
|
+
"ClassName": remote_data["ClassName"],
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
root_children.append({
|
|
49
|
+
"Name": module_name,
|
|
50
|
+
"ClassName": "Folder",
|
|
51
|
+
"Children": module_children,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
model = {
|
|
55
|
+
"ClassName": "Folder",
|
|
56
|
+
"Children": root_children,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ctx.dump_json("shared", "Remotes.model.json", model)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load(self, ctx: ExtensionRegistry):
|
|
63
|
+
ctx.register_anot(
|
|
64
|
+
AnnotationDef('remote', scope='method', retention='init', args=[str], on_build=self.remote_on_build)
|
|
65
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from .api.annotations import ENVIRONMENTS, ExtensionRegistry
|
|
9
|
+
from .build_process import BuildCtxList, BuildProcessCtx, Config, Environment, Extension, PostProcessCtx, Workspace, get_template
|
|
10
|
+
from .exceptions import BuildError, ConfigError
|
|
11
|
+
from .extensions import default as default_ext
|
|
12
|
+
|
|
13
|
+
WATCH_FILENAMES = ('*.lua', '*.luau')
|
|
14
|
+
|
|
15
|
+
def create_config(workdir: Path, config_file: Path):
|
|
16
|
+
if not config_file.exists():
|
|
17
|
+
default_config = get_template('annotations.config.json')
|
|
18
|
+
config_file.write_text(default_config)
|
|
19
|
+
print('Created a default config file')
|
|
20
|
+
else:
|
|
21
|
+
print('Config file already exists. Skipping')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def iter_rel_paths(path_map: dict[str, str], workdir: Path, env: Environment):
|
|
25
|
+
for path, lua_expr in path_map.items():
|
|
26
|
+
if '@' in path:
|
|
27
|
+
p, lua_expr = process_tags(path, lua_expr, env, workdir)
|
|
28
|
+
else:
|
|
29
|
+
p = workdir / Path(path)
|
|
30
|
+
|
|
31
|
+
if not p.is_dir():
|
|
32
|
+
print(f'WARNING: directory {p.as_posix()} does not exist')
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
yield p, lua_expr
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def import_extension_from_path(workdir: Path, entry: str):
|
|
39
|
+
workdir = workdir.resolve()
|
|
40
|
+
p = (workdir / entry).resolve()
|
|
41
|
+
|
|
42
|
+
if str(workdir) not in sys.path:
|
|
43
|
+
sys.path.insert(0, str(workdir))
|
|
44
|
+
importlib.invalidate_caches()
|
|
45
|
+
|
|
46
|
+
mod_name = '.'.join(p.relative_to(workdir).with_suffix('').parts)
|
|
47
|
+
return importlib.import_module(mod_name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def import_extension(ext: Extension, workdir: Path):
|
|
51
|
+
entry_type, entry = ext
|
|
52
|
+
|
|
53
|
+
if entry_type == 'library':
|
|
54
|
+
return importlib.import_module("lua_annotations.extensions.game_framework.main")
|
|
55
|
+
return import_extension_from_path(workdir, entry)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def process_tags(raw: str, raw_expr: str, env: Environment, workdir: Path):
|
|
59
|
+
name, data = raw.split('@')
|
|
60
|
+
|
|
61
|
+
if name == 'wally':
|
|
62
|
+
package_dir_name = 'Packages' if env == 'shared' else 'ServerPackages'
|
|
63
|
+
packages = workdir / package_dir_name / '_Index'
|
|
64
|
+
ext_dir = next(packages.glob(f'*_{data}@*'), None)
|
|
65
|
+
if not ext_dir:
|
|
66
|
+
raise ConfigError(
|
|
67
|
+
f'wally package {data} not found under {packages.as_posix()}'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return ext_dir / data, f'require({raw_expr}.{data})'
|
|
71
|
+
|
|
72
|
+
raise ConfigError(f'invalid path tag: {raw}')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build(workdir: Path, config: Config):
|
|
76
|
+
init_time = datetime.now()
|
|
77
|
+
|
|
78
|
+
for raw_workspace in config.workspaces:
|
|
79
|
+
#process workspace
|
|
80
|
+
workspace: Workspace = {}
|
|
81
|
+
for env in ENVIRONMENTS:
|
|
82
|
+
path_map = raw_workspace.get(env)
|
|
83
|
+
if not path_map:
|
|
84
|
+
raise ConfigError(f'path for the `{env}` environment is not defined in the config.')
|
|
85
|
+
|
|
86
|
+
rel_paths = dict(iter_rel_paths(path_map, workdir, env))
|
|
87
|
+
workspace[env] = rel_paths
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
#load extensions
|
|
91
|
+
reg = ExtensionRegistry()
|
|
92
|
+
default_ext.load(reg)
|
|
93
|
+
|
|
94
|
+
for ext in config.extensions:
|
|
95
|
+
#py_entry
|
|
96
|
+
module = import_extension(ext, workdir)
|
|
97
|
+
load_fn = getattr(module, 'load')
|
|
98
|
+
|
|
99
|
+
if not callable(load_fn):
|
|
100
|
+
raise BuildError(f'module {ext[1]} does not have a `load()` function')
|
|
101
|
+
load_fn(reg)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
reg = reg.sort_extensions()
|
|
105
|
+
print(f'loaded {len(reg.anot_registry)} annotations')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
#env processing
|
|
109
|
+
build_contexts: BuildCtxList = {}
|
|
110
|
+
|
|
111
|
+
for env in ENVIRONMENTS:
|
|
112
|
+
path_map = workspace.get(env)
|
|
113
|
+
if not path_map:
|
|
114
|
+
raise ConfigError(f'path for the `{env}` environment is not defined in the config.')
|
|
115
|
+
|
|
116
|
+
#process output root
|
|
117
|
+
rel_paths = workspace[env]
|
|
118
|
+
root_path = next(iter(rel_paths.keys()))
|
|
119
|
+
output_root = root_path / Path(config.out_dir_name)
|
|
120
|
+
|
|
121
|
+
shutil.rmtree(output_root, True)
|
|
122
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
#create and use a ctx
|
|
125
|
+
ctx = BuildProcessCtx(reg, root_path, workspace, rel_paths, output_root, env)
|
|
126
|
+
for path in rel_paths:
|
|
127
|
+
ctx.process_dir(path)
|
|
128
|
+
|
|
129
|
+
build_contexts[env] = ctx
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# run post-build hooks
|
|
133
|
+
if build_contexts:
|
|
134
|
+
ctx = PostProcessCtx(reg, workdir, workspace, build_contexts)
|
|
135
|
+
for hook in reg.post_build_hooks:
|
|
136
|
+
hook(ctx)
|
|
137
|
+
|
|
138
|
+
#logging
|
|
139
|
+
delta = datetime.now() - init_time
|
|
140
|
+
print(f'Built in {delta.total_seconds()}s')
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
#builds a fingerprint of all the last modified times of files
|
|
144
|
+
def _watch_fingerprint(workdir: Path, config_file: Path, config: Config):
|
|
145
|
+
output_dir_name = config.out_dir_name
|
|
146
|
+
|
|
147
|
+
#track config file
|
|
148
|
+
tracked: dict[str, int] = {str(config_file): config_file.stat().st_mtime_ns}
|
|
149
|
+
|
|
150
|
+
#track workspaces
|
|
151
|
+
for workspace in config.workspaces:
|
|
152
|
+
for env in ENVIRONMENTS:
|
|
153
|
+
path_map = workspace.get(env)
|
|
154
|
+
if not path_map:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
for rel_path in path_map:
|
|
158
|
+
env_workdir = workdir / rel_path
|
|
159
|
+
if not env_workdir.exists() or not env_workdir.is_dir():
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
for pattern in WATCH_FILENAMES:
|
|
163
|
+
for file in env_workdir.rglob(pattern):
|
|
164
|
+
if output_dir_name in file.parts:
|
|
165
|
+
continue
|
|
166
|
+
tracked[str(file)] = file.stat().st_mtime_ns
|
|
167
|
+
|
|
168
|
+
return tuple(sorted(tracked.items()))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def watch(workdir: Path, config_file: Path, poll_interval: float = 1.0):
|
|
172
|
+
print('Running initial build...')
|
|
173
|
+
config = read_config(config_file)
|
|
174
|
+
build(workdir, config)
|
|
175
|
+
|
|
176
|
+
last_fingerprint = _watch_fingerprint(workdir, config_file, config)
|
|
177
|
+
print(f'Watching for changes in {workdir} (interval: {poll_interval}s). Press Ctrl+C to stop.')
|
|
178
|
+
|
|
179
|
+
#poll fingerprints every `poll_interval` seconds
|
|
180
|
+
while True:
|
|
181
|
+
time.sleep(poll_interval)
|
|
182
|
+
|
|
183
|
+
config = read_config(config_file)
|
|
184
|
+
fingerprint = _watch_fingerprint(workdir, config_file, config)
|
|
185
|
+
|
|
186
|
+
if fingerprint != last_fingerprint:
|
|
187
|
+
print('Change detected, rebuilding...')
|
|
188
|
+
build(workdir, config)
|
|
189
|
+
last_fingerprint = fingerprint
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def read_config(config_file: Path):
|
|
193
|
+
import json
|
|
194
|
+
|
|
195
|
+
if not config_file.exists():
|
|
196
|
+
raise ConfigError('Config file not found. Run the program in init mode to create one!')
|
|
197
|
+
return Config(json.loads(config_file.read_text()))
|
lua_annotations/main.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from .exceptions import LuaAnnotationsError
|
|
7
|
+
from .init_project import build, create_config, read_config, watch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
parser = argparse.ArgumentParser(prog='lua-anot')
|
|
12
|
+
parser.add_argument('mode', help='mode of the program', choices=['build', 'init', 'watch'])
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
'workdir',
|
|
15
|
+
nargs='?',
|
|
16
|
+
default='',
|
|
17
|
+
help='working directory for the program; this should be your rojo project root',
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument('-c', '--config', default='annotations.config.json', help='filename of the config file to use')
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
'--watch-interval',
|
|
22
|
+
type=float,
|
|
23
|
+
default=1.0,
|
|
24
|
+
help='polling interval in seconds when mode is watch',
|
|
25
|
+
)
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
mode: Literal['build', 'init', 'watch'] = args.mode
|
|
29
|
+
|
|
30
|
+
workdir: Path = Path.cwd() / args.workdir
|
|
31
|
+
if not workdir.exists() or not workdir.is_dir():
|
|
32
|
+
raise LuaAnnotationsError(f'Invalid workdir: {workdir}. Provide a valid project directory.')
|
|
33
|
+
|
|
34
|
+
config_file: Path = workdir / args.config
|
|
35
|
+
|
|
36
|
+
#init mode
|
|
37
|
+
if mode == 'init':
|
|
38
|
+
create_config(workdir, config_file)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
#main mode
|
|
42
|
+
if mode == 'build':
|
|
43
|
+
build(workdir, read_config(config_file))
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
#watch mode
|
|
47
|
+
watch(workdir, config_file, poll_interval=args.watch_interval)
|
|
48
|
+
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == '__main__':
|
|
53
|
+
try:
|
|
54
|
+
main()
|
|
55
|
+
except KeyboardInterrupt:
|
|
56
|
+
print('\nStopped watching.')
|
|
57
|
+
except LuaAnnotationsError as exc:
|
|
58
|
+
print(f'Error: {exc}')
|
|
59
|
+
sys.exit(1)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
4
|
+
|
|
5
|
+
from .api.annotations import AnnotationBuildCtx, AnnotationDef, ExtensionRegistry, SortedRegistry
|
|
6
|
+
from .parser_schemas import *
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from build_process import BuildProcessCtx
|
|
10
|
+
|
|
11
|
+
#helper functions
|
|
12
|
+
K = TypeVar('K')
|
|
13
|
+
V = TypeVar('V')
|
|
14
|
+
def reverse_dict(d: dict[K, V]) -> dict[V, K]:
|
|
15
|
+
return {v: k for k, v in d.items()}
|
|
16
|
+
|
|
17
|
+
def set_adornee(anots: list[Annotation], adornee: Adornee):
|
|
18
|
+
for anot in anots:
|
|
19
|
+
anot.adornee = adornee
|
|
20
|
+
|
|
21
|
+
def remove_whitespace(t: list[Any]):
|
|
22
|
+
return [p.strip() for p in t]
|
|
23
|
+
|
|
24
|
+
def map_param_list(params: list[str]):
|
|
25
|
+
out: dict[str, str] = {}
|
|
26
|
+
for param in params:
|
|
27
|
+
parts = remove_whitespace(param.split(':'))
|
|
28
|
+
if len(parts) > 1:
|
|
29
|
+
out[parts[0]] = parts[1]
|
|
30
|
+
else:
|
|
31
|
+
out[parts[0]] = 'any'
|
|
32
|
+
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
#parsing
|
|
36
|
+
@dataclass
|
|
37
|
+
class FileParser():
|
|
38
|
+
reg: SortedRegistry
|
|
39
|
+
file: Path
|
|
40
|
+
build_ctx: 'BuildProcessCtx'
|
|
41
|
+
annotations: list[Annotation] = field(default_factory=list)
|
|
42
|
+
cur_annotations: list[Annotation] = field(default_factory=list)
|
|
43
|
+
modules: dict[str, LuaModule] = field(default_factory=dict)
|
|
44
|
+
types: dict[str, LuaType] = field(default_factory=dict)
|
|
45
|
+
cur_line = 0
|
|
46
|
+
|
|
47
|
+
def __post_init__(self):
|
|
48
|
+
self.file_name = self.file.name.split('.')[0]
|
|
49
|
+
|
|
50
|
+
#assertion functions
|
|
51
|
+
def _check_anot_scopes(self, line: str, anots: list[AnnotationDef]):
|
|
52
|
+
scope = anots[0].scope
|
|
53
|
+
for anot in anots:
|
|
54
|
+
if not anot.scope == scope:
|
|
55
|
+
self.error(line, f'all annotations must have scope: `{scope}`')
|
|
56
|
+
|
|
57
|
+
def _check_anot_relations(self, line: str, anots: list[AnnotationDef]):
|
|
58
|
+
for anot in anots:
|
|
59
|
+
for inc in anot.mutual_exclude:
|
|
60
|
+
if inc in anots:
|
|
61
|
+
self.error(line, f'annotation {anot.name} excludes {inc.name}, but it is present in this code block')
|
|
62
|
+
|
|
63
|
+
for inc in anot.mutual_include:
|
|
64
|
+
if not inc in anots:
|
|
65
|
+
self.error(line, f'annotation {anot.name} requires {inc.name}, but it is not present in this code block')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
#parsing helpers
|
|
69
|
+
def _parse_anot_args(self, adef: AnnotationDef, args: list[str]):
|
|
70
|
+
kwargs_val: dict[str, Any] = {}
|
|
71
|
+
args_val: list[Any] = []
|
|
72
|
+
|
|
73
|
+
for i, arg in enumerate(args):
|
|
74
|
+
if '=' in arg:
|
|
75
|
+
name, val = [part.strip() for part in arg.split('=', 1)]
|
|
76
|
+
proc = adef.kwargs[name]
|
|
77
|
+
kwargs_val[name] = proc(val)
|
|
78
|
+
else:
|
|
79
|
+
proc = adef.args[i]
|
|
80
|
+
args_val.append(proc(arg))
|
|
81
|
+
|
|
82
|
+
return args_val, kwargs_val
|
|
83
|
+
|
|
84
|
+
def _parse_annotation(self, text: str, ctx: ExtensionRegistry):
|
|
85
|
+
parts = remove_whitespace(ANNOTATION_ARG_RE.split(text.removeprefix(ANNOTATION_PREFIX)))
|
|
86
|
+
name = parts[0]
|
|
87
|
+
|
|
88
|
+
adef = ctx.anot_registry.get(name)
|
|
89
|
+
if adef:
|
|
90
|
+
args, kwargs = self._parse_anot_args(adef, parts[1:])
|
|
91
|
+
return Annotation(adef, name, args, kwargs)
|
|
92
|
+
else:
|
|
93
|
+
self.error(text, 'Annotation does not exist')
|
|
94
|
+
|
|
95
|
+
def _get_dict_data(self, text: str):
|
|
96
|
+
matches = DICT_REGEX.findall(text)
|
|
97
|
+
if not len(matches) > 0:
|
|
98
|
+
self.error(text, 'line is not a dict')
|
|
99
|
+
|
|
100
|
+
keys: list[str] = [m[0] for m in matches]
|
|
101
|
+
values: list[str] = [m[1] for m in matches]
|
|
102
|
+
|
|
103
|
+
if len(keys) == len(values):
|
|
104
|
+
out: dict[str, str] = {}
|
|
105
|
+
for i, key in enumerate(keys):
|
|
106
|
+
out[key] = values[i].strip().removesuffix('}').strip()
|
|
107
|
+
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
def _get_returned(self, text: str, default_name: str):
|
|
111
|
+
match = RETURN_REGEX.search(text)
|
|
112
|
+
if not match:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
single: str = match.group(2)
|
|
116
|
+
|
|
117
|
+
if single:
|
|
118
|
+
return ReturnDefinition(default_name, 'single', single_module=single)
|
|
119
|
+
else:
|
|
120
|
+
tablestr: str = match.group(1)
|
|
121
|
+
if not tablestr:
|
|
122
|
+
self.error(text, 'module export is incorrectly defined')
|
|
123
|
+
|
|
124
|
+
dict_data = self._get_dict_data(tablestr)
|
|
125
|
+
if dict_data:
|
|
126
|
+
return ReturnDefinition(default_name, 'dict', dict_val=reverse_dict(dict_data))
|
|
127
|
+
else:
|
|
128
|
+
self.error(text, 'module export is not a table')
|
|
129
|
+
|
|
130
|
+
def _get_returned_value(self, text: str, returned: ReturnDefinition):
|
|
131
|
+
match = VARIABLE_REGEX.search(text)
|
|
132
|
+
if not match:
|
|
133
|
+
self.error(text, 'code block is not a variable declaration')
|
|
134
|
+
|
|
135
|
+
name: str = match.group(1)
|
|
136
|
+
returned_name, is_submodule = returned.get_returned_name(name)
|
|
137
|
+
|
|
138
|
+
if not (name and returned_name):
|
|
139
|
+
self.error(text, 'invalid returned value definition or it is not exported.')
|
|
140
|
+
|
|
141
|
+
return ReturnedValue(self.file, name, returned_name, is_submodule)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _get_function(self, text: str, modules: dict[str, LuaModule]):
|
|
145
|
+
match = FUNCTION_REGEX.search(text)
|
|
146
|
+
if not match:
|
|
147
|
+
self.error(text, 'function is incorrectly defined')
|
|
148
|
+
|
|
149
|
+
module_name: str = match.group(1)
|
|
150
|
+
fun_name: str = match.group(2)
|
|
151
|
+
raw_params: str = match.group(3)
|
|
152
|
+
return_type: str = match.group(4) or 'any'
|
|
153
|
+
|
|
154
|
+
if not (module_name or fun_name):
|
|
155
|
+
self.error(text, 'method is incorrectly defined')
|
|
156
|
+
|
|
157
|
+
if not raw_params.strip() == '':
|
|
158
|
+
params = remove_whitespace(raw_params.split(','))
|
|
159
|
+
param_dict = map_param_list(params)
|
|
160
|
+
else:
|
|
161
|
+
param_dict = {}
|
|
162
|
+
|
|
163
|
+
if not module_name in modules:
|
|
164
|
+
self.error(module_name, 'cannot use method annotations for an unindexed module.')
|
|
165
|
+
return LuaMethod(fun_name, modules[module_name], param_dict, return_type)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#main functions
|
|
169
|
+
def error(self, text: str, message: str):
|
|
170
|
+
raise LuaParserError(message, text, self.cur_line, self.file_name)
|
|
171
|
+
|
|
172
|
+
def parse(self, text: str):
|
|
173
|
+
returned = self._get_returned(text, self.file_name)
|
|
174
|
+
if not returned:
|
|
175
|
+
print(f'Skipping file {self.file_name}; doesn\'t return a value')
|
|
176
|
+
return
|
|
177
|
+
lines = [l.rstrip() for l in text.splitlines()]
|
|
178
|
+
|
|
179
|
+
for i, line in enumerate(lines):
|
|
180
|
+
self.cur_line += 1
|
|
181
|
+
#skip empty lines
|
|
182
|
+
if line == '':
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
#comments
|
|
186
|
+
elif line.startswith('--'):
|
|
187
|
+
#annotation
|
|
188
|
+
if line.startswith(ANNOTATION_PREFIX):
|
|
189
|
+
anot = self._parse_annotation(line, self.reg)
|
|
190
|
+
if anot:
|
|
191
|
+
self.cur_annotations.append(anot)
|
|
192
|
+
else:
|
|
193
|
+
self.error(line, 'Not an annotation')
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
#if there are annotations in this block of code, then find adornee
|
|
197
|
+
if len(self.cur_annotations) > 0:
|
|
198
|
+
adefs = [anot.adef for anot in self.cur_annotations]
|
|
199
|
+
|
|
200
|
+
self._check_anot_relations(line, adefs)
|
|
201
|
+
self._check_anot_scopes(line, adefs)
|
|
202
|
+
|
|
203
|
+
scope = adefs[0].scope
|
|
204
|
+
|
|
205
|
+
#strip comments
|
|
206
|
+
line = line.split('--')[0]
|
|
207
|
+
|
|
208
|
+
#methods
|
|
209
|
+
if scope == 'method':
|
|
210
|
+
method = self._get_function(line, self.modules)
|
|
211
|
+
set_adornee(self.cur_annotations, method)
|
|
212
|
+
|
|
213
|
+
#module
|
|
214
|
+
elif scope == 'module':
|
|
215
|
+
match = MODULE_REGEX.search(line)
|
|
216
|
+
if not match:
|
|
217
|
+
self.error(line, 'code block is not a module')
|
|
218
|
+
|
|
219
|
+
name: str = match.group(1)
|
|
220
|
+
returned_name, is_submodule = returned.get_returned_name(name)
|
|
221
|
+
|
|
222
|
+
if not (name and returned_name):
|
|
223
|
+
self.error(line, 'invalid module definition or it is not returned.')
|
|
224
|
+
|
|
225
|
+
module = LuaModule(self.file, name, returned_name, is_submodule)
|
|
226
|
+
set_adornee(self.cur_annotations, module)
|
|
227
|
+
self.modules[module.name] = module
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
#returned value
|
|
231
|
+
elif scope == 'returned_value':
|
|
232
|
+
returned_value = self._get_returned_value(line, returned)
|
|
233
|
+
set_adornee(self.cur_annotations, returned_value)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
#type
|
|
237
|
+
elif scope == 'type':
|
|
238
|
+
#get entire code block
|
|
239
|
+
block = ''
|
|
240
|
+
for line2 in lines[i:]:
|
|
241
|
+
block += line2 + '\n'
|
|
242
|
+
if '}' in line2:
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
#use type regex
|
|
246
|
+
match = TYPE_REGEX.search(block)
|
|
247
|
+
if not match:
|
|
248
|
+
self.error(line, 'code block is not a type definition')
|
|
249
|
+
|
|
250
|
+
exported = bool(match.group(1))
|
|
251
|
+
name: str = match.group(2)
|
|
252
|
+
contents: str = match.group(3)
|
|
253
|
+
|
|
254
|
+
if not (name and contents):
|
|
255
|
+
self.error(line, 'type definition is missing name or contents')
|
|
256
|
+
|
|
257
|
+
if contents.startswith('{'):
|
|
258
|
+
data = self._get_dict_data(contents)
|
|
259
|
+
else:
|
|
260
|
+
data = contents
|
|
261
|
+
|
|
262
|
+
if not data:
|
|
263
|
+
self.error(line, 'type definition is missing type data')
|
|
264
|
+
|
|
265
|
+
lua_type = LuaType(name, data, exported)
|
|
266
|
+
|
|
267
|
+
set_adornee(self.cur_annotations, lua_type)
|
|
268
|
+
self.types[name] = lua_type
|
|
269
|
+
|
|
270
|
+
#now run anot on_build
|
|
271
|
+
for anot in self.cur_annotations:
|
|
272
|
+
for adef in [anot.adef] + anot.adef.extends:
|
|
273
|
+
if adef.on_build:
|
|
274
|
+
adef.on_build(AnnotationBuildCtx(anot, self, self.build_ctx))
|
|
275
|
+
|
|
276
|
+
self.annotations += self.cur_annotations
|
|
277
|
+
self.cur_annotations = []
|