pyvbaanalysis 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.
- pyvbaanalysis/__init__.py +59 -0
- pyvbaanalysis/__main__.py +8 -0
- pyvbaanalysis/call/__init__.py +23 -0
- pyvbaanalysis/call/call_context.py +296 -0
- pyvbaanalysis/cli.py +352 -0
- pyvbaanalysis/completion/__init__.py +49 -0
- pyvbaanalysis/completion/cursor_context.py +26 -0
- pyvbaanalysis/completion/event_handlers.py +82 -0
- pyvbaanalysis/completion/member_access.py +1097 -0
- pyvbaanalysis/completion/type_completion.py +228 -0
- pyvbaanalysis/conditional/__init__.py +41 -0
- pyvbaanalysis/conditional/conditional_compilation.py +531 -0
- pyvbaanalysis/constants/__init__.py +21 -0
- pyvbaanalysis/constants/integer_constant_expression.py +226 -0
- pyvbaanalysis/data/event_definitions.json +1 -0
- pyvbaanalysis/data/excel_host_model.json +1 -0
- pyvbaanalysis/data/manifest.json +257 -0
- pyvbaanalysis/data/rule_metadata.json +1287 -0
- pyvbaanalysis/data/vba_runtime_tables.json +1 -0
- pyvbaanalysis/diagnostics/__init__.py +59 -0
- pyvbaanalysis/diagnostics/analyze_module.py +177 -0
- pyvbaanalysis/diagnostics/argument_inference.py +706 -0
- pyvbaanalysis/diagnostics/call_extraction.py +380 -0
- pyvbaanalysis/diagnostics/callable_signatures.py +443 -0
- pyvbaanalysis/diagnostics/const_expr.py +134 -0
- pyvbaanalysis/diagnostics/context.py +130 -0
- pyvbaanalysis/diagnostics/dataflow.py +242 -0
- pyvbaanalysis/diagnostics/exprwalk.py +110 -0
- pyvbaanalysis/diagnostics/model.py +108 -0
- pyvbaanalysis/diagnostics/registry.py +263 -0
- pyvbaanalysis/diagnostics/rule_metadata.py +243 -0
- pyvbaanalysis/diagnostics/rules/__init__.py +2 -0
- pyvbaanalysis/diagnostics/rules/argument_shape.py +204 -0
- pyvbaanalysis/diagnostics/rules/argument_types.py +80 -0
- pyvbaanalysis/diagnostics/rules/arrays.py +1000 -0
- pyvbaanalysis/diagnostics/rules/assignments.py +650 -0
- pyvbaanalysis/diagnostics/rules/binary_operand_scalar.py +75 -0
- pyvbaanalysis/diagnostics/rules/call_arity.py +114 -0
- pyvbaanalysis/diagnostics/rules/control_flow.py +651 -0
- pyvbaanalysis/diagnostics/rules/declarations.py +1642 -0
- pyvbaanalysis/diagnostics/rules/duplicates.py +311 -0
- pyvbaanalysis/diagnostics/rules/expressions.py +679 -0
- pyvbaanalysis/diagnostics/rules/lexical.py +185 -0
- pyvbaanalysis/diagnostics/rules/module_kind.py +389 -0
- pyvbaanalysis/diagnostics/rules/numeric_literals.py +49 -0
- pyvbaanalysis/diagnostics/rules/object_state.py +327 -0
- pyvbaanalysis/diagnostics/rules/parameter_defaults.py +92 -0
- pyvbaanalysis/diagnostics/rules/runtime_values.py +382 -0
- pyvbaanalysis/diagnostics/rules/shared.py +441 -0
- pyvbaanalysis/diagnostics/rules/type_of_is.py +312 -0
- pyvbaanalysis/diagnostics/rules/undeclared.py +503 -0
- pyvbaanalysis/diagnostics/walker.py +347 -0
- pyvbaanalysis/evidence.py +111 -0
- pyvbaanalysis/flow/__init__.py +21 -0
- pyvbaanalysis/flow/procedure_labels.py +274 -0
- pyvbaanalysis/flow/procedure_unstructured.py +65 -0
- pyvbaanalysis/host/__init__.py +41 -0
- pyvbaanalysis/host/host_model.py +209 -0
- pyvbaanalysis/lexer/__init__.py +43 -0
- pyvbaanalysis/lexer/keyword_table.py +141 -0
- pyvbaanalysis/lexer/stripped_lines.py +41 -0
- pyvbaanalysis/lexer/token_helpers.py +115 -0
- pyvbaanalysis/lexer/token_kinds.py +113 -0
- pyvbaanalysis/lexer/tokenize.py +413 -0
- pyvbaanalysis/lexer/trivia.py +65 -0
- pyvbaanalysis/parser/__init__.py +22 -0
- pyvbaanalysis/parser/fixed_length_string.py +58 -0
- pyvbaanalysis/parser/nodes.py +721 -0
- pyvbaanalysis/parser/parse_expression.py +621 -0
- pyvbaanalysis/parser/parse_module.py +1472 -0
- pyvbaanalysis/parser/parser_state.py +110 -0
- pyvbaanalysis/parser/type_declaration_suffix.py +29 -0
- pyvbaanalysis/project.py +146 -0
- pyvbaanalysis/py.typed +0 -0
- pyvbaanalysis/reader/__init__.py +49 -0
- pyvbaanalysis/reader/loose_file.py +104 -0
- pyvbaanalysis/reader/vbe_module.py +137 -0
- pyvbaanalysis/reader/workbook.py +104 -0
- pyvbaanalysis/runtime/__init__.py +29 -0
- pyvbaanalysis/runtime/vba_runtime.py +314 -0
- pyvbaanalysis/symbols/__init__.py +60 -0
- pyvbaanalysis/symbols/build_module_symbols.py +379 -0
- pyvbaanalysis/symbols/name_resolution.py +252 -0
- pyvbaanalysis/symbols/project_index.py +942 -0
- pyvbaanalysis/symbols/symbol_model.py +371 -0
- pyvbaanalysis/types/__init__.py +17 -0
- pyvbaanalysis/types/type_inference.py +278 -0
- pyvbaanalysis/types/type_names.py +105 -0
- pyvbaanalysis-1.0.0.dist-info/METADATA +120 -0
- pyvbaanalysis-1.0.0.dist-info/RECORD +93 -0
- pyvbaanalysis-1.0.0.dist-info/WHEEL +4 -0
- pyvbaanalysis-1.0.0.dist-info/entry_points.txt +2 -0
- pyvbaanalysis-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""pyVBAanalysis: pure-Python static analysis for Excel VBA.
|
|
2
|
+
|
|
3
|
+
It reports a diagnostic only when it is provably correct; anything unknown or
|
|
4
|
+
ambiguous stays quiet (a no-false-positive discipline).
|
|
5
|
+
|
|
6
|
+
The headline entry points are re-exported here for convenience:
|
|
7
|
+
|
|
8
|
+
* analyze_module(source, opts): analyze one module's source text.
|
|
9
|
+
* analyze_project(modules): analyze a set of modules with cross-module context.
|
|
10
|
+
* analyze_workbook(path): analyze the VBA in an Excel workbook (read via pyOpenVBA,
|
|
11
|
+
the one external runtime dependency).
|
|
12
|
+
* analyze_loose_file(path) / analyze_loose_files(paths): analyze loose
|
|
13
|
+
.bas / .cls / .frm export files.
|
|
14
|
+
|
|
15
|
+
The full analysis model (tokens, AST, symbols, types) lives in the submodules
|
|
16
|
+
(pyvbaanalysis.lexer, .parser, .symbols, .diagnostics, and the rest); see
|
|
17
|
+
docs/api-reference.md for the import map.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from .conditional import DEFAULT_COMPILER_CONSTANTS, ConditionalCompilationEnvironment
|
|
23
|
+
from .diagnostics import (
|
|
24
|
+
AnalyzeModuleOptions,
|
|
25
|
+
DiagnosticSeverity,
|
|
26
|
+
VbaDiagnostic,
|
|
27
|
+
analyze_module,
|
|
28
|
+
line_col,
|
|
29
|
+
rule_metadata_by_code,
|
|
30
|
+
validate_severity_overrides,
|
|
31
|
+
)
|
|
32
|
+
from .project import analyze_module_options_for, analyze_project, build_project_index
|
|
33
|
+
from .reader import analyze_loose_file, analyze_loose_files, analyze_workbook
|
|
34
|
+
from .symbols import ModuleInput, ModuleSymbolKind, ProjectIndex, ProjectIndexOptions
|
|
35
|
+
|
|
36
|
+
__version__ = "1.0.0"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"AnalyzeModuleOptions",
|
|
40
|
+
"ConditionalCompilationEnvironment",
|
|
41
|
+
"DEFAULT_COMPILER_CONSTANTS",
|
|
42
|
+
"DiagnosticSeverity",
|
|
43
|
+
"ModuleInput",
|
|
44
|
+
"ModuleSymbolKind",
|
|
45
|
+
"ProjectIndex",
|
|
46
|
+
"ProjectIndexOptions",
|
|
47
|
+
"VbaDiagnostic",
|
|
48
|
+
"__version__",
|
|
49
|
+
"analyze_loose_file",
|
|
50
|
+
"analyze_loose_files",
|
|
51
|
+
"analyze_module",
|
|
52
|
+
"analyze_module_options_for",
|
|
53
|
+
"analyze_project",
|
|
54
|
+
"analyze_workbook",
|
|
55
|
+
"build_project_index",
|
|
56
|
+
"line_col",
|
|
57
|
+
"rule_metadata_by_code",
|
|
58
|
+
"validate_severity_overrides",
|
|
59
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Shared VBA call-site/context helpers (ported from src/analyzer/call)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .call_context import (
|
|
6
|
+
BareCallStatementTarget,
|
|
7
|
+
ExplicitCallStatementArgumentList,
|
|
8
|
+
ParenthesizedCallStatementTarget,
|
|
9
|
+
bare_call_statement_target,
|
|
10
|
+
explicit_call_statement_argument_without_parens,
|
|
11
|
+
explicit_call_statement_target,
|
|
12
|
+
standalone_empty_parenthesized_call_statement,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BareCallStatementTarget",
|
|
17
|
+
"ExplicitCallStatementArgumentList",
|
|
18
|
+
"ParenthesizedCallStatementTarget",
|
|
19
|
+
"bare_call_statement_target",
|
|
20
|
+
"explicit_call_statement_argument_without_parens",
|
|
21
|
+
"explicit_call_statement_target",
|
|
22
|
+
"standalone_empty_parenthesized_call_statement",
|
|
23
|
+
]
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Shared VBA call-site/context helpers.
|
|
2
|
+
|
|
3
|
+
Ported from xlide_vscode/src/analyzer/call/callContext.ts. VBA distinguishes
|
|
4
|
+
expression calls, explicit ``Call`` statements, and parenless call statements,
|
|
5
|
+
and those contexts have different parenthesis rules. This module owns the
|
|
6
|
+
classification the diagnostics call rules depend on; the completion / signature-
|
|
7
|
+
help surfaces in the TS file are not needed by the analyzer port.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from ..lexer.keyword_table import STATEMENT_KEYWORDS as _STATEMENT_KEYWORD_LIST
|
|
16
|
+
from ..lexer.token_helpers import (
|
|
17
|
+
match_paren_from,
|
|
18
|
+
statement_tokens,
|
|
19
|
+
token_name,
|
|
20
|
+
token_word,
|
|
21
|
+
tokens_without_leading_line_number,
|
|
22
|
+
)
|
|
23
|
+
from ..lexer.token_kinds import TokenKind, VbaToken
|
|
24
|
+
from ..lexer.tokenize import tokenize
|
|
25
|
+
from ..parser.nodes import Span
|
|
26
|
+
|
|
27
|
+
# Statement-like words that are not in MS-VBAL's statement-keyword list but still
|
|
28
|
+
# cannot be treated as bare parenless call targets.
|
|
29
|
+
_ADDITIONAL_NON_CALL_STATEMENT_LEADS = (
|
|
30
|
+
"then", "property", "error", "line", "name", "kill",
|
|
31
|
+
"mkdir", "rmdir", "chdir", "chdrive", "load", "unload",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Lowercase statement-leading words that must not be parsed as bare calls.
|
|
35
|
+
STATEMENT_KEYWORDS: frozenset[str] = frozenset(
|
|
36
|
+
[word.lower() for word in _STATEMENT_KEYWORD_LIST]
|
|
37
|
+
) | frozenset(_ADDITIONAL_NON_CALL_STATEMENT_LEADS)
|
|
38
|
+
|
|
39
|
+
_WHITESPACE = re.compile(r"\s")
|
|
40
|
+
_DIGITS = re.compile(r"^\d+$")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class BareCallStatementTarget:
|
|
45
|
+
name: str
|
|
46
|
+
span: Span
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ParenthesizedCallStatementTarget:
|
|
51
|
+
name: str
|
|
52
|
+
span: Span
|
|
53
|
+
empty_parens_span: Span
|
|
54
|
+
is_member: bool
|
|
55
|
+
starts_with_leading_dot: bool
|
|
56
|
+
callee_end_offset: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class ExplicitCallStatementArgumentList:
|
|
61
|
+
callee_end_offset: int
|
|
62
|
+
first_argument_span: Span
|
|
63
|
+
argument_span: Span
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _statement_tokens_after_leading_line_number(source: str, span: Span) -> list[VbaToken]:
|
|
67
|
+
return tokens_without_leading_line_number(statement_tokens(source, span.start, span.end))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _leading_line_number_token_count(tokens: list[VbaToken]) -> int:
|
|
71
|
+
return (
|
|
72
|
+
1
|
|
73
|
+
if len(tokens) > 1
|
|
74
|
+
and tokens[0].kind is TokenKind.INTEGER_LITERAL
|
|
75
|
+
and _DIGITS.match(tokens[0].raw_text)
|
|
76
|
+
else 0
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def bare_call_statement_target(source: str, span: Span) -> BareCallStatementTarget | None:
|
|
81
|
+
"""The callee of a bare call statement, or None.
|
|
82
|
+
|
|
83
|
+
Member calls, assignments, labels, statement keywords, and implicit
|
|
84
|
+
Application index/member forms are intentionally excluded.
|
|
85
|
+
"""
|
|
86
|
+
toks = _statement_tokens_after_leading_line_number(source, span)
|
|
87
|
+
if not toks:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
idx = 0
|
|
91
|
+
explicit_call = token_word(toks[0]) == "call"
|
|
92
|
+
if explicit_call:
|
|
93
|
+
idx = 1
|
|
94
|
+
|
|
95
|
+
callee = toks[idx] if idx < len(toks) else None
|
|
96
|
+
if callee is None or callee.kind is not TokenKind.IDENTIFIER:
|
|
97
|
+
return None
|
|
98
|
+
if callee.raw_text.lower() in STATEMENT_KEYWORDS:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
result = BareCallStatementTarget(
|
|
102
|
+
name=callee.raw_text,
|
|
103
|
+
span=Span(span.start + callee.start, span.start + callee.end),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
next_tok = toks[idx + 1] if idx + 1 < len(toks) else None
|
|
107
|
+
if next_tok is None:
|
|
108
|
+
if not explicit_call:
|
|
109
|
+
j = span.start + callee.end
|
|
110
|
+
while j < len(source) and source[j] in (" ", "\t"):
|
|
111
|
+
j += 1
|
|
112
|
+
if j < len(source) and source[j] == ":":
|
|
113
|
+
return None
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
r = next_tok.raw_text
|
|
117
|
+
if r in (".", ":"):
|
|
118
|
+
return None
|
|
119
|
+
if not explicit_call and r == "(":
|
|
120
|
+
return None
|
|
121
|
+
if not explicit_call:
|
|
122
|
+
gap = source[span.start + callee.end : span.start + next_tok.start]
|
|
123
|
+
if _WHITESPACE.search(gap) is None:
|
|
124
|
+
return None
|
|
125
|
+
depth = 0
|
|
126
|
+
for k in range(idx + 1, len(toks)):
|
|
127
|
+
tr = toks[k].raw_text
|
|
128
|
+
if tr in ("(", "["):
|
|
129
|
+
depth += 1
|
|
130
|
+
elif tr in (")", "]"):
|
|
131
|
+
depth -= 1
|
|
132
|
+
elif depth == 0 and tr == "=":
|
|
133
|
+
return None
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def explicit_call_statement_target(source: str, span: Span) -> BareCallStatementTarget | None:
|
|
138
|
+
"""The callee name and span of an explicit ``Call`` statement, or None when the statement is not a Call."""
|
|
139
|
+
toks = _statement_tokens_after_leading_line_number(source, span)
|
|
140
|
+
if len(toks) < 2 or token_word(toks[0]) != "call":
|
|
141
|
+
return None
|
|
142
|
+
name = token_name(toks[1])
|
|
143
|
+
if not name:
|
|
144
|
+
return None
|
|
145
|
+
return BareCallStatementTarget(
|
|
146
|
+
name=name, span=Span(span.start + toks[1].start, span.start + toks[1].end)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def explicit_call_statement_argument_without_parens(source: str, span: Span) -> Span | None:
|
|
151
|
+
"""The span of the first stray argument when an explicit ``Call`` passes arguments without enclosing parentheses, or None."""
|
|
152
|
+
found = _explicit_call_statement_argument_list_without_parens(source, span)
|
|
153
|
+
return found.first_argument_span if found is not None else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _explicit_call_statement_argument_list_without_parens(
|
|
157
|
+
source: str, span: Span
|
|
158
|
+
) -> ExplicitCallStatementArgumentList | None:
|
|
159
|
+
raw_toks = [t for t in tokenize(source[span.start : span.end]) if t.kind is not TokenKind.NEWLINE]
|
|
160
|
+
toks = [t for t in raw_toks if t.kind is not TokenKind.COMMENT]
|
|
161
|
+
start = _leading_line_number_token_count(toks)
|
|
162
|
+
if len(toks) == start or token_word(toks[start]) != "call":
|
|
163
|
+
return None
|
|
164
|
+
consumed = _consume_callable_chain(toks, start + 1)
|
|
165
|
+
if consumed is None or consumed <= start + 1:
|
|
166
|
+
return None
|
|
167
|
+
stray = toks[consumed] if consumed < len(toks) else None
|
|
168
|
+
if stray is None or stray.raw_text == ":":
|
|
169
|
+
return None
|
|
170
|
+
end = span.end
|
|
171
|
+
for tok in raw_toks:
|
|
172
|
+
if tok.start < stray.start:
|
|
173
|
+
continue
|
|
174
|
+
if tok.kind is TokenKind.COMMENT:
|
|
175
|
+
end = span.start + tok.start
|
|
176
|
+
break
|
|
177
|
+
if tok.raw_text == ":":
|
|
178
|
+
return None
|
|
179
|
+
while end > span.start and source[end - 1] in (" ", "\t"):
|
|
180
|
+
end -= 1
|
|
181
|
+
arg_start = span.start + stray.start
|
|
182
|
+
if end <= arg_start:
|
|
183
|
+
return None
|
|
184
|
+
callee = toks[consumed - 1]
|
|
185
|
+
return ExplicitCallStatementArgumentList(
|
|
186
|
+
callee_end_offset=span.start + callee.end,
|
|
187
|
+
first_argument_span=Span(arg_start, span.start + stray.end),
|
|
188
|
+
argument_span=Span(arg_start, end),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def standalone_empty_parenthesized_call_statement(
|
|
193
|
+
source: str, span: Span
|
|
194
|
+
) -> ParenthesizedCallStatementTarget | None:
|
|
195
|
+
"""A parenless call statement whose callee is immediately followed by an empty ``()`` and nothing else, or None (e.g. ``Foo()`` or ``obj.Foo()`` as a whole statement)."""
|
|
196
|
+
toks = _statement_tokens_after_leading_line_number(source, span)
|
|
197
|
+
if len(toks) < 3 or token_word(toks[0]) == "call" or _top_level_token_index(toks, "=") >= 0:
|
|
198
|
+
return None
|
|
199
|
+
for i in range(len(toks) - 2):
|
|
200
|
+
name = token_name(toks[i])
|
|
201
|
+
if not name or toks[i + 1].raw_text != "(":
|
|
202
|
+
continue
|
|
203
|
+
close = match_paren_from(toks, i + 1)
|
|
204
|
+
if (
|
|
205
|
+
close != i + 2
|
|
206
|
+
or close != len(toks) - 1
|
|
207
|
+
or not _is_complete_statement_chain_through_empty_call(toks, i, close)
|
|
208
|
+
):
|
|
209
|
+
continue
|
|
210
|
+
return ParenthesizedCallStatementTarget(
|
|
211
|
+
name=name,
|
|
212
|
+
is_member=i > 0 and toks[i - 1].raw_text == ".",
|
|
213
|
+
starts_with_leading_dot=toks[0].raw_text == ".",
|
|
214
|
+
callee_end_offset=span.start + toks[i].end,
|
|
215
|
+
empty_parens_span=Span(span.start + toks[i + 1].start, span.start + toks[close].end),
|
|
216
|
+
span=Span(span.start + toks[i].start, span.start + toks[close].end),
|
|
217
|
+
)
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _consume_callable_chain(tokens: list[VbaToken], start: int) -> int | None:
|
|
222
|
+
if start >= len(tokens) or not token_name(tokens[start]):
|
|
223
|
+
return None
|
|
224
|
+
i = start + 1
|
|
225
|
+
while True:
|
|
226
|
+
t = tokens[i] if i < len(tokens) else None
|
|
227
|
+
if t is None:
|
|
228
|
+
return i
|
|
229
|
+
if t.raw_text == ".":
|
|
230
|
+
if i + 1 >= len(tokens) or not token_name(tokens[i + 1]):
|
|
231
|
+
return i
|
|
232
|
+
i += 2
|
|
233
|
+
continue
|
|
234
|
+
if t.raw_text == "(":
|
|
235
|
+
close = match_paren_from(tokens, i)
|
|
236
|
+
if close < 0:
|
|
237
|
+
return None
|
|
238
|
+
i = close + 1
|
|
239
|
+
continue
|
|
240
|
+
return i
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _is_complete_statement_chain_through_empty_call(
|
|
244
|
+
toks: list[VbaToken], callee_idx: int, close_idx: int
|
|
245
|
+
) -> bool:
|
|
246
|
+
if callee_idx == 0:
|
|
247
|
+
return bool(token_name(toks[0]))
|
|
248
|
+
first = toks[0]
|
|
249
|
+
i = 1
|
|
250
|
+
if first.raw_text == ".":
|
|
251
|
+
name_idx = 1
|
|
252
|
+
if name_idx >= len(toks) or not token_name(toks[name_idx]):
|
|
253
|
+
return False
|
|
254
|
+
if name_idx == callee_idx:
|
|
255
|
+
return (
|
|
256
|
+
name_idx + 1 < len(toks)
|
|
257
|
+
and toks[name_idx + 1].raw_text == "("
|
|
258
|
+
and match_paren_from(toks, name_idx + 1) == close_idx
|
|
259
|
+
)
|
|
260
|
+
i = name_idx + 1
|
|
261
|
+
elif not token_name(first):
|
|
262
|
+
return False
|
|
263
|
+
while i < len(toks):
|
|
264
|
+
raw = toks[i].raw_text
|
|
265
|
+
if raw == "(":
|
|
266
|
+
close = match_paren_from(toks, i)
|
|
267
|
+
if close < 0 or close >= callee_idx:
|
|
268
|
+
return False
|
|
269
|
+
i = close + 1
|
|
270
|
+
continue
|
|
271
|
+
if raw != ".":
|
|
272
|
+
return False
|
|
273
|
+
name_idx = i + 1
|
|
274
|
+
if name_idx >= len(toks) or not token_name(toks[name_idx]):
|
|
275
|
+
return False
|
|
276
|
+
if name_idx == callee_idx:
|
|
277
|
+
return (
|
|
278
|
+
name_idx + 1 < len(toks)
|
|
279
|
+
and toks[name_idx + 1].raw_text == "("
|
|
280
|
+
and match_paren_from(toks, name_idx + 1) == close_idx
|
|
281
|
+
)
|
|
282
|
+
i = name_idx + 1
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _top_level_token_index(tokens: list[VbaToken], raw_text: str) -> int:
|
|
287
|
+
depth = 0
|
|
288
|
+
for i, tok in enumerate(tokens):
|
|
289
|
+
raw = tok.raw_text
|
|
290
|
+
if raw in ("(", "["):
|
|
291
|
+
depth += 1
|
|
292
|
+
elif raw in (")", "]"):
|
|
293
|
+
depth -= 1
|
|
294
|
+
elif depth == 0 and raw == raw_text:
|
|
295
|
+
return i
|
|
296
|
+
return -1
|