python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.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.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
delphi_lsp/metrics.py
ADDED
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .consts import AttributeName, SyntaxNodeType
|
|
11
|
+
from .lark_tokens import KEYWORDS
|
|
12
|
+
from .nodes import SyntaxNode, ValuedSyntaxNode
|
|
13
|
+
from .parser import DelphiParseError, DelphiParser
|
|
14
|
+
from .parser_backend import ParserMode, normalize_mode
|
|
15
|
+
from .preprocessor import IncludeLoader
|
|
16
|
+
from .semantic import SymbolKind
|
|
17
|
+
|
|
18
|
+
_KEYWORDS = frozenset(keyword.casefold() for keyword in KEYWORDS)
|
|
19
|
+
_TWO_CHARACTER_OPERATORS = frozenset({":=", "<=", ">=", "<>", "..", "**"})
|
|
20
|
+
_LINE_SCAN_SPECIAL_RE = re.compile(r"//|\{|\(\*|'|\r\n|\r|\n")
|
|
21
|
+
_NON_WHITESPACE_RE = re.compile(r"\S")
|
|
22
|
+
_WHITESPACE_RE = re.compile(r"\s+")
|
|
23
|
+
_IDENTIFIER_PART_RE = re.compile(r"\w*")
|
|
24
|
+
_NON_DECIMAL_DIGIT_CLASS = (
|
|
25
|
+
r"\u00B2-\u00B3\u00B9\u1369-\u1371\u19DA\u2070\u2074-\u2079"
|
|
26
|
+
r"\u2080-\u2089\u2460-\u2468\u2474-\u247C\u2488-\u2490\u24EA"
|
|
27
|
+
r"\u24F5-\u24FD\u24FF\u2776-\u277E\u2780-\u2788\u278A-\u2792"
|
|
28
|
+
r"\U00010A40-\U00010A43\U00010E60-\U00010E68"
|
|
29
|
+
r"\U00011052-\U0001105A\U0001F100-\U0001F10A"
|
|
30
|
+
)
|
|
31
|
+
_WORD_OPERATOR_CLASS = (
|
|
32
|
+
r"\u00BC-\u00BE\u09F4-\u09F9\u0B72-\u0B77\u0BF0-\u0BF2"
|
|
33
|
+
r"\u0C78-\u0C7E\u0D58-\u0D5E\u0D70-\u0D78\u0F2A-\u0F33"
|
|
34
|
+
r"\u1372-\u137C\u16EE-\u16F0\u17F0-\u17F9\u2150-\u2182"
|
|
35
|
+
r"\u2185-\u2189\u2469-\u2473\u247D-\u2487\u2491-\u249B"
|
|
36
|
+
r"\u24EB-\u24F4\u24FE\u277F\u2789\u2793\u2CFD\u3007"
|
|
37
|
+
r"\u3021-\u3029\u3038-\u303A\u3192-\u3195\u3220-\u3229"
|
|
38
|
+
r"\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF"
|
|
39
|
+
r"\uA6E6-\uA6EF\uA830-\uA835\U00010107-\U00010133"
|
|
40
|
+
r"\U00010140-\U00010178\U0001018A-\U0001018B"
|
|
41
|
+
r"\U000102E1-\U000102FB\U00010320-\U00010323\U00010341"
|
|
42
|
+
r"\U0001034A\U000103D1-\U000103D5\U00010858-\U0001085F"
|
|
43
|
+
r"\U00010879-\U0001087F\U000108A7-\U000108AF"
|
|
44
|
+
r"\U000108FB-\U000108FF\U00010916-\U0001091B"
|
|
45
|
+
r"\U000109BC-\U000109BD\U000109C0-\U000109CF"
|
|
46
|
+
r"\U000109D2-\U000109FF\U00010A44-\U00010A48"
|
|
47
|
+
r"\U00010A7D-\U00010A7E\U00010A9D-\U00010A9F"
|
|
48
|
+
r"\U00010AEB-\U00010AEF\U00010B58-\U00010B5F"
|
|
49
|
+
r"\U00010B78-\U00010B7F\U00010BA9-\U00010BAF"
|
|
50
|
+
r"\U00010CFA-\U00010CFF\U00010E69-\U00010E7E"
|
|
51
|
+
r"\U00010F1D-\U00010F26\U00010F51-\U00010F54"
|
|
52
|
+
r"\U00010FC5-\U00010FCB\U0001105B-\U00011065"
|
|
53
|
+
r"\U000111E1-\U000111F4\U0001173A-\U0001173B"
|
|
54
|
+
r"\U000118EA-\U000118F2\U00011C5A-\U00011C6C"
|
|
55
|
+
r"\U00011FC0-\U00011FD4\U00012400-\U0001246E"
|
|
56
|
+
r"\U00016B5B-\U00016B61\U00016E80-\U00016E96"
|
|
57
|
+
r"\U0001D2C0-\U0001D2D3\U0001D2E0-\U0001D2F3"
|
|
58
|
+
r"\U0001D360-\U0001D378\U0001E8C7-\U0001E8CF"
|
|
59
|
+
r"\U0001EC71-\U0001ECAB\U0001ECAD-\U0001ECAF"
|
|
60
|
+
r"\U0001ECB1-\U0001ECB4\U0001ED01-\U0001ED2D"
|
|
61
|
+
r"\U0001ED2F-\U0001ED3D\U0001F10B-\U0001F10C"
|
|
62
|
+
)
|
|
63
|
+
_METRIC_TOKEN_RE = re.compile(
|
|
64
|
+
r"//[^\r\n]*"
|
|
65
|
+
r"|\{[^}]*\}?"
|
|
66
|
+
r"|\(\*.*?(?:\*\)|\Z)"
|
|
67
|
+
r"|('(?:''|[^'])*(?:'|\Z)"
|
|
68
|
+
rf"|&(?=\d|[{_NON_DECIMAL_DIGIT_CLASS}{_WORD_OPERATOR_CLASS}])"
|
|
69
|
+
r"|[$%#](?=\w)(?:\w|\.(?!\.)|(?<=[eE])[+-])*"
|
|
70
|
+
r"|&?[A-Za-z_]\w*"
|
|
71
|
+
rf"|[{_NON_DECIMAL_DIGIT_CLASS}]"
|
|
72
|
+
r"(?:\w|\.(?!\.)|(?<=[eE])[+-])*"
|
|
73
|
+
r"|\d(?:\w|\.(?!\.)|(?<=[eE])[+-])*"
|
|
74
|
+
rf"|[{_WORD_OPERATOR_CLASS}]"
|
|
75
|
+
r"|&?\w+"
|
|
76
|
+
r"|:=|<=|>=|<>|\.\.|\*\*"
|
|
77
|
+
r"|\S)",
|
|
78
|
+
re.DOTALL,
|
|
79
|
+
)
|
|
80
|
+
_ASCII_METRIC_TOKEN_RE = re.compile(
|
|
81
|
+
r"//[^\r\n]*"
|
|
82
|
+
r"|\{[^}]*\}?"
|
|
83
|
+
r"|\(\*.*?(?:\*\)|\Z)"
|
|
84
|
+
r"|('(?:''|[^'])*(?:'|\Z)"
|
|
85
|
+
r"|&(?=\d)"
|
|
86
|
+
r"|[$%#](?=\w)(?:\w|\.(?!\.)|(?<=[eE])[+-])*"
|
|
87
|
+
r"|\d(?:\w|\.(?!\.)|(?<=[eE])[+-])*"
|
|
88
|
+
r"|&?[A-Za-z_]\w*"
|
|
89
|
+
r"|:=|<=|>=|<>|\.\.|\*\*"
|
|
90
|
+
r"|\S)",
|
|
91
|
+
re.DOTALL,
|
|
92
|
+
)
|
|
93
|
+
_DECISION_NODES = frozenset(
|
|
94
|
+
{
|
|
95
|
+
SyntaxNodeType.ntIf,
|
|
96
|
+
SyntaxNodeType.ntFor,
|
|
97
|
+
SyntaxNodeType.ntWhile,
|
|
98
|
+
SyntaxNodeType.ntRepeat,
|
|
99
|
+
SyntaxNodeType.ntCaseSelector,
|
|
100
|
+
SyntaxNodeType.ntExceptionHandler,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
_DEPENDENCY_NODES = frozenset(
|
|
104
|
+
{
|
|
105
|
+
SyntaxNodeType.ntUses,
|
|
106
|
+
SyntaxNodeType.ntContains,
|
|
107
|
+
SyntaxNodeType.ntRequires,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
_LARGE_PROJECT_UNIT_THRESHOLD = 256
|
|
111
|
+
_LARGE_PROJECT_CHAR_THRESHOLD = 16 * 1024 * 1024
|
|
112
|
+
_METRIC_TOKEN_CHUNK_TARGET = 590 * 1024
|
|
113
|
+
_METRIC_TOKEN_FINDALL_LIMIT = 600 * 1024
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class MetricProblem:
|
|
118
|
+
kind: str
|
|
119
|
+
path: str
|
|
120
|
+
message: str
|
|
121
|
+
|
|
122
|
+
def to_mapping(self) -> dict[str, str]:
|
|
123
|
+
return {"kind": self.kind, "path": self.path, "message": self.message}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class LineMetrics:
|
|
128
|
+
total_lines: int = 0
|
|
129
|
+
source_lines: int = 0
|
|
130
|
+
blank_lines: int = 0
|
|
131
|
+
comment_only_lines: int = 0
|
|
132
|
+
comment_lines: int = 0
|
|
133
|
+
directive_lines: int = 0
|
|
134
|
+
|
|
135
|
+
def to_mapping(self) -> dict[str, int]:
|
|
136
|
+
return {
|
|
137
|
+
"total_lines": self.total_lines,
|
|
138
|
+
"source_lines": self.source_lines,
|
|
139
|
+
"blank_lines": self.blank_lines,
|
|
140
|
+
"comment_only_lines": self.comment_only_lines,
|
|
141
|
+
"comment_lines": self.comment_lines,
|
|
142
|
+
"directive_lines": self.directive_lines,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True)
|
|
147
|
+
class HalsteadMetrics:
|
|
148
|
+
distinct_operators: int = 0
|
|
149
|
+
distinct_operands: int = 0
|
|
150
|
+
total_operators: int = 0
|
|
151
|
+
total_operands: int = 0
|
|
152
|
+
vocabulary: int = 0
|
|
153
|
+
length: int = 0
|
|
154
|
+
calculated_length: float = 0.0
|
|
155
|
+
volume: float = 0.0
|
|
156
|
+
difficulty: float = 0.0
|
|
157
|
+
effort: float = 0.0
|
|
158
|
+
estimated_time_seconds: float = 0.0
|
|
159
|
+
estimated_defects: float = 0.0
|
|
160
|
+
|
|
161
|
+
def to_mapping(self) -> dict[str, int | float]:
|
|
162
|
+
return {
|
|
163
|
+
"distinct_operators": self.distinct_operators,
|
|
164
|
+
"distinct_operands": self.distinct_operands,
|
|
165
|
+
"total_operators": self.total_operators,
|
|
166
|
+
"total_operands": self.total_operands,
|
|
167
|
+
"vocabulary": self.vocabulary,
|
|
168
|
+
"length": self.length,
|
|
169
|
+
"calculated_length": self.calculated_length,
|
|
170
|
+
"volume": self.volume,
|
|
171
|
+
"difficulty": self.difficulty,
|
|
172
|
+
"effort": self.effort,
|
|
173
|
+
"estimated_time_seconds": self.estimated_time_seconds,
|
|
174
|
+
"estimated_defects": self.estimated_defects,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True)
|
|
179
|
+
class RoutineComplexity:
|
|
180
|
+
name: str
|
|
181
|
+
value: int
|
|
182
|
+
line: int
|
|
183
|
+
|
|
184
|
+
def to_mapping(self) -> dict[str, str | int]:
|
|
185
|
+
return {"name": self.name, "value": self.value, "line": self.line}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True)
|
|
189
|
+
class CyclomaticMetrics:
|
|
190
|
+
routine_count: int = 0
|
|
191
|
+
total: int = 0
|
|
192
|
+
average: float = 0.0
|
|
193
|
+
maximum: int = 0
|
|
194
|
+
routines: tuple[RoutineComplexity, ...] = ()
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_routines(cls, routines: Iterable[RoutineComplexity]) -> CyclomaticMetrics:
|
|
198
|
+
ordered = tuple(
|
|
199
|
+
sorted(
|
|
200
|
+
routines,
|
|
201
|
+
key=lambda item: (-item.value, item.name.casefold(), item.line, item.name),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
total = sum(item.value for item in ordered)
|
|
205
|
+
count = len(ordered)
|
|
206
|
+
return cls(
|
|
207
|
+
routine_count=count,
|
|
208
|
+
total=total,
|
|
209
|
+
average=total / count if count else 0.0,
|
|
210
|
+
maximum=max((item.value for item in ordered), default=0),
|
|
211
|
+
routines=ordered,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def to_mapping(self, *, detail: bool = False) -> dict[str, object]:
|
|
215
|
+
result: dict[str, object] = {
|
|
216
|
+
"routine_count": self.routine_count,
|
|
217
|
+
"total": self.total,
|
|
218
|
+
"average": self.average,
|
|
219
|
+
"maximum": self.maximum,
|
|
220
|
+
}
|
|
221
|
+
if detail:
|
|
222
|
+
result["routines"] = [item.to_mapping() for item in self.routines]
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True)
|
|
227
|
+
class UnitMetrics:
|
|
228
|
+
name: str
|
|
229
|
+
path: str
|
|
230
|
+
lines: LineMetrics
|
|
231
|
+
cyclomatic: CyclomaticMetrics
|
|
232
|
+
halstead: HalsteadMetrics
|
|
233
|
+
maintainability_index: float
|
|
234
|
+
symbol_counts: Mapping[str, int]
|
|
235
|
+
dependencies: tuple[str, ...] = ()
|
|
236
|
+
internal_dependencies: tuple[str, ...] = ()
|
|
237
|
+
external_dependencies: tuple[str, ...] = ()
|
|
238
|
+
afferent_coupling: int = 0
|
|
239
|
+
efferent_coupling: int = 0
|
|
240
|
+
instability: float = 0.0
|
|
241
|
+
class_like_types: int = 0
|
|
242
|
+
abstract_types: int = 0
|
|
243
|
+
abstractness: float = 0.0
|
|
244
|
+
distance: float = 1.0
|
|
245
|
+
problems: tuple[MetricProblem, ...] = ()
|
|
246
|
+
unit_id: str = ""
|
|
247
|
+
_operator_vocabulary: frozenset[str] = field(default_factory=frozenset, repr=False)
|
|
248
|
+
_operand_vocabulary: frozenset[str] = field(default_factory=frozenset, repr=False)
|
|
249
|
+
|
|
250
|
+
def to_mapping(self, *, detail: bool = False) -> dict[str, object]:
|
|
251
|
+
result: dict[str, object] = {
|
|
252
|
+
"name": self.name,
|
|
253
|
+
"path": self.path,
|
|
254
|
+
"unit_id": self.unit_id,
|
|
255
|
+
"lines": self.lines.to_mapping(),
|
|
256
|
+
"cyclomatic": self.cyclomatic.to_mapping(detail=detail),
|
|
257
|
+
"maintainability_index": self.maintainability_index,
|
|
258
|
+
"afferent_coupling": self.afferent_coupling,
|
|
259
|
+
"efferent_coupling": self.efferent_coupling,
|
|
260
|
+
"instability": self.instability,
|
|
261
|
+
"abstractness": self.abstractness,
|
|
262
|
+
"distance": self.distance,
|
|
263
|
+
"class_like_types": self.class_like_types,
|
|
264
|
+
"abstract_types": self.abstract_types,
|
|
265
|
+
"problems": [problem.to_mapping() for problem in self.problems],
|
|
266
|
+
}
|
|
267
|
+
if detail:
|
|
268
|
+
result.update(
|
|
269
|
+
{
|
|
270
|
+
"halstead": self.halstead.to_mapping(),
|
|
271
|
+
"symbol_counts": dict(sorted(self.symbol_counts.items())),
|
|
272
|
+
"dependencies": list(self.dependencies),
|
|
273
|
+
"internal_dependencies": list(self.internal_dependencies),
|
|
274
|
+
"external_dependencies": list(self.external_dependencies),
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
result["halstead_volume"] = self.halstead.volume
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(frozen=True)
|
|
283
|
+
class ProjectMetrics:
|
|
284
|
+
units: tuple[UnitMetrics, ...]
|
|
285
|
+
lines: LineMetrics
|
|
286
|
+
cyclomatic: CyclomaticMetrics
|
|
287
|
+
halstead: HalsteadMetrics
|
|
288
|
+
maintainability_index: float
|
|
289
|
+
total_loc: int
|
|
290
|
+
include_loc: int
|
|
291
|
+
total_loc_with_includes: int
|
|
292
|
+
dependency_edges: int
|
|
293
|
+
problems: tuple[MetricProblem, ...] = ()
|
|
294
|
+
project_id: str = ""
|
|
295
|
+
project_name: str = ""
|
|
296
|
+
include_metrics: tuple[UnitMetrics, ...] = ()
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def unit_count(self) -> int:
|
|
300
|
+
return len(self.units)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def include_file_count(self) -> int:
|
|
304
|
+
return len(self.include_metrics)
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def file_metrics(self) -> tuple[UnitMetrics, ...]:
|
|
308
|
+
return tuple(
|
|
309
|
+
sorted(
|
|
310
|
+
(*self.units, *self.include_metrics),
|
|
311
|
+
key=lambda item: (
|
|
312
|
+
item.name.casefold(),
|
|
313
|
+
item.path.casefold(),
|
|
314
|
+
item.name,
|
|
315
|
+
item.path,
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def unit_by_name(self, name: str) -> UnitMetrics:
|
|
321
|
+
normalized = name.casefold()
|
|
322
|
+
for unit in self.units:
|
|
323
|
+
if unit.name.casefold() == normalized:
|
|
324
|
+
return unit
|
|
325
|
+
raise KeyError(name)
|
|
326
|
+
|
|
327
|
+
def to_mapping(self) -> dict[str, object]:
|
|
328
|
+
return {
|
|
329
|
+
"project_id": self.project_id,
|
|
330
|
+
"project_name": self.project_name,
|
|
331
|
+
"unit_count": self.unit_count,
|
|
332
|
+
"include_file_count": self.include_file_count,
|
|
333
|
+
"total_loc": self.total_loc,
|
|
334
|
+
"include_loc": self.include_loc,
|
|
335
|
+
"total_loc_with_includes": self.total_loc_with_includes,
|
|
336
|
+
"lines": self.lines.to_mapping(),
|
|
337
|
+
"cyclomatic": self.cyclomatic.to_mapping(),
|
|
338
|
+
"halstead": self.halstead.to_mapping(),
|
|
339
|
+
"maintainability_index": self.maintainability_index,
|
|
340
|
+
"dependency_edges": self.dependency_edges,
|
|
341
|
+
"problems": [problem.to_mapping() for problem in self.problems],
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass(frozen=True)
|
|
346
|
+
class _ScanResult:
|
|
347
|
+
lines: LineMetrics
|
|
348
|
+
operator_vocabulary: frozenset[str]
|
|
349
|
+
operand_vocabulary: frozenset[str]
|
|
350
|
+
total_operators: int
|
|
351
|
+
total_operands: int
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def analyze_unit(
|
|
355
|
+
source: str,
|
|
356
|
+
path: str,
|
|
357
|
+
*,
|
|
358
|
+
defines: Iterable[str] = (),
|
|
359
|
+
include_paths: Iterable[str] = (),
|
|
360
|
+
include_loader: IncludeLoader | None = None,
|
|
361
|
+
parser_mode: ParserMode | str = ParserMode.STRICT,
|
|
362
|
+
) -> UnitMetrics:
|
|
363
|
+
scan = _scan_source(source)
|
|
364
|
+
problems: list[MetricProblem] = []
|
|
365
|
+
root: SyntaxNode | None = None
|
|
366
|
+
try:
|
|
367
|
+
parsed = DelphiParser(
|
|
368
|
+
defines=tuple(defines),
|
|
369
|
+
include_paths=tuple(include_paths),
|
|
370
|
+
include_loader=include_loader,
|
|
371
|
+
mode=normalize_mode(parser_mode),
|
|
372
|
+
).parse(
|
|
373
|
+
source,
|
|
374
|
+
path,
|
|
375
|
+
build_semantic=False,
|
|
376
|
+
build_source_map=False,
|
|
377
|
+
build_expressions=False,
|
|
378
|
+
build_node_positions=False,
|
|
379
|
+
build_comments=False,
|
|
380
|
+
)
|
|
381
|
+
root = parsed.root
|
|
382
|
+
if parsed.problems:
|
|
383
|
+
first = parsed.problems[0]
|
|
384
|
+
problems.append(
|
|
385
|
+
MetricProblem(
|
|
386
|
+
kind="partial_parse",
|
|
387
|
+
path=path,
|
|
388
|
+
message=(
|
|
389
|
+
f"Partial DelphiAST parse at {first.line}:{first.column}: "
|
|
390
|
+
f"{first.message}"
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
except DelphiParseError as error:
|
|
395
|
+
problems.append(
|
|
396
|
+
MetricProblem(
|
|
397
|
+
kind="partial_parse",
|
|
398
|
+
path=path,
|
|
399
|
+
message=f"Recovered from strict parse failure: {error}",
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
try:
|
|
403
|
+
parsed = DelphiParser(
|
|
404
|
+
defines=tuple(defines),
|
|
405
|
+
include_paths=tuple(include_paths),
|
|
406
|
+
include_loader=include_loader,
|
|
407
|
+
mode=ParserMode.TOLERANT,
|
|
408
|
+
legacy_fallback_max_chars=0,
|
|
409
|
+
).parse(
|
|
410
|
+
source,
|
|
411
|
+
path,
|
|
412
|
+
build_semantic=False,
|
|
413
|
+
build_source_map=False,
|
|
414
|
+
build_expressions=False,
|
|
415
|
+
build_node_positions=False,
|
|
416
|
+
build_comments=False,
|
|
417
|
+
)
|
|
418
|
+
root = parsed.root
|
|
419
|
+
except Exception as recovery_error:
|
|
420
|
+
problems.append(
|
|
421
|
+
MetricProblem(
|
|
422
|
+
kind="parse_error",
|
|
423
|
+
path=path,
|
|
424
|
+
message=(
|
|
425
|
+
"Could not recover source "
|
|
426
|
+
f"({type(recovery_error).__name__})."
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
except Exception as error:
|
|
431
|
+
problems.append(
|
|
432
|
+
MetricProblem(
|
|
433
|
+
kind="parse_error",
|
|
434
|
+
path=path,
|
|
435
|
+
message=f"Could not parse source ({type(error).__name__}).",
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
name = Path(path).stem
|
|
440
|
+
dependencies: tuple[str, ...] = ()
|
|
441
|
+
cyclomatic = CyclomaticMetrics()
|
|
442
|
+
symbol_counts: Counter[str] = Counter()
|
|
443
|
+
class_like_types = 0
|
|
444
|
+
abstract_types = 0
|
|
445
|
+
if root is not None:
|
|
446
|
+
name = root.get_attribute(AttributeName.anName) or name
|
|
447
|
+
dependencies = _dependencies(root)
|
|
448
|
+
cyclomatic = _cyclomatic_metrics(root)
|
|
449
|
+
class_like_types, abstract_types = _abstract_type_counts(root)
|
|
450
|
+
symbol_counts = _syntax_symbol_counts(root)
|
|
451
|
+
|
|
452
|
+
halstead = _halstead_metrics(
|
|
453
|
+
operator_vocabulary=scan.operator_vocabulary,
|
|
454
|
+
operand_vocabulary=scan.operand_vocabulary,
|
|
455
|
+
total_operators=scan.total_operators,
|
|
456
|
+
total_operands=scan.total_operands,
|
|
457
|
+
)
|
|
458
|
+
abstractness = abstract_types / class_like_types if class_like_types else 0.0
|
|
459
|
+
maintainability = _maintainability_index(
|
|
460
|
+
volume=halstead.volume,
|
|
461
|
+
complexity=cyclomatic.total,
|
|
462
|
+
source_lines=scan.lines.source_lines,
|
|
463
|
+
routine_count=cyclomatic.routine_count,
|
|
464
|
+
)
|
|
465
|
+
return UnitMetrics(
|
|
466
|
+
name=name,
|
|
467
|
+
path=path,
|
|
468
|
+
lines=scan.lines,
|
|
469
|
+
cyclomatic=cyclomatic,
|
|
470
|
+
halstead=halstead,
|
|
471
|
+
maintainability_index=maintainability,
|
|
472
|
+
symbol_counts=dict(sorted(symbol_counts.items())),
|
|
473
|
+
dependencies=dependencies,
|
|
474
|
+
class_like_types=class_like_types,
|
|
475
|
+
abstract_types=abstract_types,
|
|
476
|
+
abstractness=abstractness,
|
|
477
|
+
distance=abs(abstractness - 1.0),
|
|
478
|
+
problems=tuple(problems),
|
|
479
|
+
_operator_vocabulary=scan.operator_vocabulary,
|
|
480
|
+
_operand_vocabulary=scan.operand_vocabulary,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def analyze_include(source: str, path: str) -> UnitMetrics:
|
|
485
|
+
"""Measure a standalone include fragment without invoking the parser."""
|
|
486
|
+
|
|
487
|
+
scan = _scan_source(source, collect_tokens=False)
|
|
488
|
+
halstead = HalsteadMetrics()
|
|
489
|
+
return UnitMetrics(
|
|
490
|
+
name=Path(path).name,
|
|
491
|
+
path=path,
|
|
492
|
+
lines=scan.lines,
|
|
493
|
+
cyclomatic=CyclomaticMetrics(),
|
|
494
|
+
halstead=halstead,
|
|
495
|
+
maintainability_index=_maintainability_index(
|
|
496
|
+
volume=halstead.volume,
|
|
497
|
+
complexity=0,
|
|
498
|
+
source_lines=scan.lines.source_lines,
|
|
499
|
+
routine_count=0,
|
|
500
|
+
),
|
|
501
|
+
symbol_counts={},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def analyze_project(
|
|
506
|
+
sources: Mapping[str, str],
|
|
507
|
+
*,
|
|
508
|
+
include_sources: Mapping[str, str] | None = None,
|
|
509
|
+
defines: Iterable[str] = (),
|
|
510
|
+
include_paths: Iterable[str] = (),
|
|
511
|
+
project_id: str = "",
|
|
512
|
+
project_name: str = "",
|
|
513
|
+
unit_ids: Mapping[str, str] | None = None,
|
|
514
|
+
parser_mode: ParserMode | str | None = None,
|
|
515
|
+
) -> ProjectMetrics:
|
|
516
|
+
unique_sources = _unique_sources(sources)
|
|
517
|
+
if parser_mode is None:
|
|
518
|
+
source_chars = sum(len(source) for source in unique_sources.values())
|
|
519
|
+
selected_mode = (
|
|
520
|
+
ParserMode.TOLERANT
|
|
521
|
+
if len(unique_sources) >= _LARGE_PROJECT_UNIT_THRESHOLD
|
|
522
|
+
or source_chars >= _LARGE_PROJECT_CHAR_THRESHOLD
|
|
523
|
+
else ParserMode.STRICT
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
selected_mode = normalize_mode(parser_mode)
|
|
527
|
+
units = [
|
|
528
|
+
analyze_unit(
|
|
529
|
+
source,
|
|
530
|
+
path,
|
|
531
|
+
defines=defines,
|
|
532
|
+
include_paths=include_paths,
|
|
533
|
+
parser_mode=selected_mode,
|
|
534
|
+
)
|
|
535
|
+
for path, source in unique_sources.items()
|
|
536
|
+
]
|
|
537
|
+
unique_includes = _unique_sources(include_sources or {})
|
|
538
|
+
include_metrics = tuple(
|
|
539
|
+
analyze_include(source, path)
|
|
540
|
+
for path, source in unique_includes.items()
|
|
541
|
+
)
|
|
542
|
+
include_loc = sum(item.lines.total_lines for item in include_metrics)
|
|
543
|
+
return aggregate_project_metrics(
|
|
544
|
+
units,
|
|
545
|
+
include_loc=include_loc,
|
|
546
|
+
include_metrics=include_metrics,
|
|
547
|
+
project_id=project_id,
|
|
548
|
+
project_name=project_name,
|
|
549
|
+
unit_ids=unit_ids,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def aggregate_project_metrics(
|
|
554
|
+
analyzed_units: Iterable[UnitMetrics],
|
|
555
|
+
*,
|
|
556
|
+
include_loc: int = 0,
|
|
557
|
+
include_metrics: Iterable[UnitMetrics] = (),
|
|
558
|
+
project_id: str = "",
|
|
559
|
+
project_name: str = "",
|
|
560
|
+
unit_ids: Mapping[str, str] | None = None,
|
|
561
|
+
) -> ProjectMetrics:
|
|
562
|
+
units = list(analyzed_units)
|
|
563
|
+
include_items = tuple(
|
|
564
|
+
sorted(
|
|
565
|
+
include_metrics,
|
|
566
|
+
key=lambda item: (
|
|
567
|
+
item.name.casefold(),
|
|
568
|
+
item.path.casefold(),
|
|
569
|
+
item.name,
|
|
570
|
+
item.path,
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
units.sort(
|
|
575
|
+
key=lambda item: (
|
|
576
|
+
item.name.casefold(),
|
|
577
|
+
item.path.casefold(),
|
|
578
|
+
item.name,
|
|
579
|
+
item.path,
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
canonical_units: dict[str, UnitMetrics] = {}
|
|
584
|
+
for unit in units:
|
|
585
|
+
canonical_units.setdefault(unit.name.casefold(), unit)
|
|
586
|
+
|
|
587
|
+
reverse_edges: dict[str, set[str]] = {name: set() for name in canonical_units}
|
|
588
|
+
dependencies_by_unit: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {}
|
|
589
|
+
dependency_edges = 0
|
|
590
|
+
for unit in units:
|
|
591
|
+
internal: dict[str, str] = {}
|
|
592
|
+
external: dict[str, str] = {}
|
|
593
|
+
for dependency in unit.dependencies:
|
|
594
|
+
normalized_dependency = dependency.casefold()
|
|
595
|
+
if normalized_dependency == unit.name.casefold():
|
|
596
|
+
continue
|
|
597
|
+
if normalized_dependency in canonical_units:
|
|
598
|
+
canonical_name = canonical_units[normalized_dependency].name
|
|
599
|
+
internal.setdefault(normalized_dependency, canonical_name)
|
|
600
|
+
reverse_edges[normalized_dependency].add(unit.name.casefold())
|
|
601
|
+
else:
|
|
602
|
+
external.setdefault(normalized_dependency, dependency)
|
|
603
|
+
internal_values = tuple(sorted(internal.values(), key=lambda value: (value.casefold(), value)))
|
|
604
|
+
external_values = tuple(sorted(external.values(), key=lambda value: (value.casefold(), value)))
|
|
605
|
+
dependencies_by_unit[unit.path] = (internal_values, external_values)
|
|
606
|
+
dependency_edges += len(internal_values) + len(external_values)
|
|
607
|
+
|
|
608
|
+
operator_vocabulary = frozenset().union(
|
|
609
|
+
*(unit._operator_vocabulary for unit in units)
|
|
610
|
+
)
|
|
611
|
+
operand_vocabulary = frozenset().union(
|
|
612
|
+
*(unit._operand_vocabulary for unit in units)
|
|
613
|
+
)
|
|
614
|
+
total_operators = sum(unit.halstead.total_operators for unit in units)
|
|
615
|
+
total_operands = sum(unit.halstead.total_operands for unit in units)
|
|
616
|
+
|
|
617
|
+
enriched: list[UnitMetrics] = []
|
|
618
|
+
id_by_path = {path.casefold(): value for path, value in (unit_ids or {}).items()}
|
|
619
|
+
for unit in units:
|
|
620
|
+
internal, external = dependencies_by_unit[unit.path]
|
|
621
|
+
afferent = len(reverse_edges.get(unit.name.casefold(), ()))
|
|
622
|
+
efferent = len(internal) + len(external)
|
|
623
|
+
instability = efferent / (afferent + efferent) if afferent + efferent else 0.0
|
|
624
|
+
enriched.append(
|
|
625
|
+
replace(
|
|
626
|
+
unit,
|
|
627
|
+
internal_dependencies=internal,
|
|
628
|
+
external_dependencies=external,
|
|
629
|
+
afferent_coupling=afferent,
|
|
630
|
+
efferent_coupling=efferent,
|
|
631
|
+
instability=instability,
|
|
632
|
+
distance=abs(unit.abstractness + instability - 1.0),
|
|
633
|
+
unit_id=id_by_path.get(unit.path.casefold(), unit.unit_id),
|
|
634
|
+
_operator_vocabulary=frozenset(),
|
|
635
|
+
_operand_vocabulary=frozenset(),
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
line_metrics = _sum_line_metrics(unit.lines for unit in enriched)
|
|
640
|
+
routines = [routine for unit in enriched for routine in unit.cyclomatic.routines]
|
|
641
|
+
cyclomatic = CyclomaticMetrics.from_routines(routines)
|
|
642
|
+
halstead = _halstead_metrics(
|
|
643
|
+
operator_vocabulary=operator_vocabulary,
|
|
644
|
+
operand_vocabulary=operand_vocabulary,
|
|
645
|
+
total_operators=total_operators,
|
|
646
|
+
total_operands=total_operands,
|
|
647
|
+
)
|
|
648
|
+
maintainability = (
|
|
649
|
+
sum(
|
|
650
|
+
unit.maintainability_index * unit.lines.source_lines
|
|
651
|
+
for unit in enriched
|
|
652
|
+
)
|
|
653
|
+
/ line_metrics.source_lines
|
|
654
|
+
if line_metrics.source_lines
|
|
655
|
+
else 100.0
|
|
656
|
+
)
|
|
657
|
+
problems = tuple(problem for unit in enriched for problem in unit.problems)
|
|
658
|
+
return ProjectMetrics(
|
|
659
|
+
units=tuple(enriched),
|
|
660
|
+
lines=line_metrics,
|
|
661
|
+
cyclomatic=cyclomatic,
|
|
662
|
+
halstead=halstead,
|
|
663
|
+
maintainability_index=maintainability,
|
|
664
|
+
total_loc=line_metrics.total_lines,
|
|
665
|
+
include_loc=include_loc,
|
|
666
|
+
total_loc_with_includes=line_metrics.total_lines + include_loc,
|
|
667
|
+
dependency_edges=dependency_edges,
|
|
668
|
+
problems=problems,
|
|
669
|
+
project_id=project_id,
|
|
670
|
+
project_name=project_name,
|
|
671
|
+
include_metrics=include_items,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _unique_sources(sources: Mapping[str, str]) -> dict[str, str]:
|
|
676
|
+
result: dict[str, str] = {}
|
|
677
|
+
seen: set[str] = set()
|
|
678
|
+
for path, source in sorted(sources.items(), key=lambda item: (item[0].casefold(), item[0])):
|
|
679
|
+
normalized = str(Path(path)).replace("\\", "/").casefold()
|
|
680
|
+
if normalized in seen:
|
|
681
|
+
continue
|
|
682
|
+
seen.add(normalized)
|
|
683
|
+
result[path] = source
|
|
684
|
+
return result
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _scan_source(source: str, *, collect_tokens: bool = True) -> _ScanResult:
|
|
688
|
+
if not collect_tokens:
|
|
689
|
+
return _scan_source_lines(source)
|
|
690
|
+
|
|
691
|
+
if len(source) <= _METRIC_TOKEN_FINDALL_LIMIT:
|
|
692
|
+
token_chunks = [(0, len(source))] if source else []
|
|
693
|
+
line_scan = _scan_source_lines(source)
|
|
694
|
+
else:
|
|
695
|
+
token_chunks = []
|
|
696
|
+
line_scan = _scan_source_lines(source, token_chunks=token_chunks)
|
|
697
|
+
token_pattern = _ASCII_METRIC_TOKEN_RE if source.isascii() else _METRIC_TOKEN_RE
|
|
698
|
+
operators: set[str] = set()
|
|
699
|
+
operands: set[str] = set()
|
|
700
|
+
total_operators = 0
|
|
701
|
+
total_operands = 0
|
|
702
|
+
|
|
703
|
+
def record_word(value: str, occurrences: int = 1) -> None:
|
|
704
|
+
nonlocal total_operators, total_operands
|
|
705
|
+
first = value[0]
|
|
706
|
+
if (
|
|
707
|
+
first == "&"
|
|
708
|
+
and len(value) > 1
|
|
709
|
+
and _identifier_start(value[1])
|
|
710
|
+
and "." not in value
|
|
711
|
+
and "+" not in value
|
|
712
|
+
and "-" not in value
|
|
713
|
+
):
|
|
714
|
+
operands.add(value[1:].casefold())
|
|
715
|
+
total_operands += occurrences
|
|
716
|
+
return
|
|
717
|
+
if (
|
|
718
|
+
_identifier_start(first)
|
|
719
|
+
and "." not in value
|
|
720
|
+
and "+" not in value
|
|
721
|
+
and "-" not in value
|
|
722
|
+
):
|
|
723
|
+
normalized = value.casefold()
|
|
724
|
+
if normalized in _KEYWORDS:
|
|
725
|
+
operators.add(normalized)
|
|
726
|
+
total_operators += occurrences
|
|
727
|
+
else:
|
|
728
|
+
operands.add(normalized)
|
|
729
|
+
total_operands += occurrences
|
|
730
|
+
return
|
|
731
|
+
if first.isdigit() or (
|
|
732
|
+
first in {"$", "%", "#"}
|
|
733
|
+
and len(value) > 1
|
|
734
|
+
and value[1].isalnum()
|
|
735
|
+
):
|
|
736
|
+
operands.add(value.casefold())
|
|
737
|
+
total_operands += occurrences
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
index = 0
|
|
741
|
+
while index < len(value):
|
|
742
|
+
character = value[index]
|
|
743
|
+
if (
|
|
744
|
+
character == "&"
|
|
745
|
+
and index + 1 < len(value)
|
|
746
|
+
and _identifier_start(value[index + 1])
|
|
747
|
+
):
|
|
748
|
+
identifier_tail = _IDENTIFIER_PART_RE.match(value, index + 2)
|
|
749
|
+
assert identifier_tail is not None
|
|
750
|
+
end = identifier_tail.end()
|
|
751
|
+
operands.add(value[index + 1 : end].casefold())
|
|
752
|
+
total_operands += occurrences
|
|
753
|
+
index = end
|
|
754
|
+
continue
|
|
755
|
+
if _identifier_start(character):
|
|
756
|
+
identifier_tail = _IDENTIFIER_PART_RE.match(value, index + 1)
|
|
757
|
+
assert identifier_tail is not None
|
|
758
|
+
end = identifier_tail.end()
|
|
759
|
+
record_word(value[index:end], occurrences)
|
|
760
|
+
index = end
|
|
761
|
+
continue
|
|
762
|
+
if character.isdigit() or (
|
|
763
|
+
character in {"$", "%", "#"}
|
|
764
|
+
and index + 1 < len(value)
|
|
765
|
+
and value[index + 1].isalnum()
|
|
766
|
+
):
|
|
767
|
+
end = _number_end(value, index)
|
|
768
|
+
operands.add(value[index:end].casefold())
|
|
769
|
+
total_operands += occurrences
|
|
770
|
+
index = end
|
|
771
|
+
continue
|
|
772
|
+
pair = value[index : index + 2]
|
|
773
|
+
end = index + 2 if pair in _TWO_CHARACTER_OPERATORS else index + 1
|
|
774
|
+
operators.add(value[index:end].casefold())
|
|
775
|
+
total_operators += occurrences
|
|
776
|
+
index = end
|
|
777
|
+
|
|
778
|
+
token_counts: Counter[str] = Counter()
|
|
779
|
+
for start, end in token_chunks:
|
|
780
|
+
if end - start <= _METRIC_TOKEN_FINDALL_LIMIT:
|
|
781
|
+
token_counts.update(token_pattern.findall(source, start, end))
|
|
782
|
+
else:
|
|
783
|
+
token_counts.update(
|
|
784
|
+
match.group(1) or ""
|
|
785
|
+
for match in token_pattern.finditer(source, start, end)
|
|
786
|
+
)
|
|
787
|
+
token_counts.pop("", None)
|
|
788
|
+
|
|
789
|
+
for value, occurrences in token_counts.items():
|
|
790
|
+
if value.startswith("'"):
|
|
791
|
+
operands.add(value)
|
|
792
|
+
total_operands += occurrences
|
|
793
|
+
continue
|
|
794
|
+
first = value[0]
|
|
795
|
+
if first == "&" or first == "_" or first.isalnum() or first in {"$", "%", "#"}:
|
|
796
|
+
record_word(value, occurrences)
|
|
797
|
+
continue
|
|
798
|
+
operators.add(value.casefold())
|
|
799
|
+
total_operators += occurrences
|
|
800
|
+
|
|
801
|
+
return _ScanResult(
|
|
802
|
+
lines=line_scan.lines,
|
|
803
|
+
operator_vocabulary=frozenset(operators),
|
|
804
|
+
operand_vocabulary=frozenset(operands),
|
|
805
|
+
total_operators=total_operators,
|
|
806
|
+
total_operands=total_operands,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _scan_source_linear(source: str, *, collect_tokens: bool = True) -> _ScanResult:
|
|
811
|
+
if not collect_tokens:
|
|
812
|
+
return _scan_source_lines(source)
|
|
813
|
+
line_count = len(source.splitlines()) if source else 0
|
|
814
|
+
has_code = [False] * line_count
|
|
815
|
+
has_comment = [False] * line_count
|
|
816
|
+
has_directive = [False] * line_count
|
|
817
|
+
operators: set[str] = set()
|
|
818
|
+
operands: set[str] = set()
|
|
819
|
+
total_operators = 0
|
|
820
|
+
total_operands = 0
|
|
821
|
+
current_line = 0
|
|
822
|
+
|
|
823
|
+
def mark_line(flags: list[bool]) -> None:
|
|
824
|
+
if current_line < len(flags):
|
|
825
|
+
flags[current_line] = True
|
|
826
|
+
|
|
827
|
+
def mark_span(flags: list[bool], start: int, end: int) -> None:
|
|
828
|
+
nonlocal current_line
|
|
829
|
+
if not flags or end <= start:
|
|
830
|
+
return
|
|
831
|
+
newline_count = _line_break_count(source, start, end)
|
|
832
|
+
end_line = current_line + newline_count
|
|
833
|
+
if source[end - 1] in {"\r", "\n"}:
|
|
834
|
+
end_line -= 1
|
|
835
|
+
for line in range(current_line, min(end_line, len(flags) - 1) + 1):
|
|
836
|
+
flags[line] = True
|
|
837
|
+
current_line += newline_count
|
|
838
|
+
|
|
839
|
+
index = 0
|
|
840
|
+
length = len(source)
|
|
841
|
+
while index < length:
|
|
842
|
+
character = source[index]
|
|
843
|
+
if character.isspace():
|
|
844
|
+
whitespace = _WHITESPACE_RE.match(source, index)
|
|
845
|
+
assert whitespace is not None
|
|
846
|
+
end = whitespace.end()
|
|
847
|
+
current_line += _line_break_count(source, index, end)
|
|
848
|
+
index = end
|
|
849
|
+
continue
|
|
850
|
+
if source.startswith("//", index):
|
|
851
|
+
end = _line_ending_start(source, index + 2)
|
|
852
|
+
mark_span(has_comment, index, end)
|
|
853
|
+
index = end
|
|
854
|
+
continue
|
|
855
|
+
if character == "{":
|
|
856
|
+
close = source.find("}", index + 1)
|
|
857
|
+
end = length if close < 0 else close + 1
|
|
858
|
+
directive = index + 1 < length and source[index + 1] == "$"
|
|
859
|
+
mark_span(has_directive if directive else has_comment, index, end)
|
|
860
|
+
index = end
|
|
861
|
+
continue
|
|
862
|
+
if source.startswith("(*", index):
|
|
863
|
+
close = source.find("*)", index + 2)
|
|
864
|
+
end = length if close < 0 else close + 2
|
|
865
|
+
directive = index + 2 < length and source[index + 2] == "$"
|
|
866
|
+
mark_span(has_directive if directive else has_comment, index, end)
|
|
867
|
+
index = end
|
|
868
|
+
continue
|
|
869
|
+
if character == "'":
|
|
870
|
+
end = _quoted_end(source, index)
|
|
871
|
+
mark_span(has_code, index, end)
|
|
872
|
+
if collect_tokens:
|
|
873
|
+
operands.add(source[index:end])
|
|
874
|
+
total_operands += 1
|
|
875
|
+
index = end
|
|
876
|
+
continue
|
|
877
|
+
if character == "&" and index + 1 < length and _identifier_start(source[index + 1]):
|
|
878
|
+
end = index + 2
|
|
879
|
+
identifier_tail = _IDENTIFIER_PART_RE.match(source, end)
|
|
880
|
+
assert identifier_tail is not None
|
|
881
|
+
end = identifier_tail.end()
|
|
882
|
+
mark_line(has_code)
|
|
883
|
+
if collect_tokens:
|
|
884
|
+
operands.add(source[index + 1 : end].casefold())
|
|
885
|
+
total_operands += 1
|
|
886
|
+
index = end
|
|
887
|
+
continue
|
|
888
|
+
if _identifier_start(character):
|
|
889
|
+
end = index + 1
|
|
890
|
+
identifier_tail = _IDENTIFIER_PART_RE.match(source, end)
|
|
891
|
+
assert identifier_tail is not None
|
|
892
|
+
end = identifier_tail.end()
|
|
893
|
+
value = source[index:end].casefold()
|
|
894
|
+
mark_line(has_code)
|
|
895
|
+
if collect_tokens:
|
|
896
|
+
if value in _KEYWORDS:
|
|
897
|
+
operators.add(value)
|
|
898
|
+
total_operators += 1
|
|
899
|
+
else:
|
|
900
|
+
operands.add(value)
|
|
901
|
+
total_operands += 1
|
|
902
|
+
index = end
|
|
903
|
+
continue
|
|
904
|
+
if character.isdigit() or (
|
|
905
|
+
character in {"$", "%", "#"}
|
|
906
|
+
and index + 1 < length
|
|
907
|
+
and source[index + 1].isalnum()
|
|
908
|
+
):
|
|
909
|
+
end = _number_end(source, index)
|
|
910
|
+
mark_line(has_code)
|
|
911
|
+
if collect_tokens:
|
|
912
|
+
operands.add(source[index:end].casefold())
|
|
913
|
+
total_operands += 1
|
|
914
|
+
index = end
|
|
915
|
+
continue
|
|
916
|
+
pair = source[index : index + 2]
|
|
917
|
+
end = index + 2 if pair in _TWO_CHARACTER_OPERATORS else index + 1
|
|
918
|
+
value = source[index:end]
|
|
919
|
+
mark_line(has_code)
|
|
920
|
+
if collect_tokens:
|
|
921
|
+
operators.add(value.casefold())
|
|
922
|
+
total_operators += 1
|
|
923
|
+
index = end
|
|
924
|
+
|
|
925
|
+
source_lines = 0
|
|
926
|
+
blank_lines = 0
|
|
927
|
+
comment_only_lines = 0
|
|
928
|
+
for line in range(line_count):
|
|
929
|
+
if has_code[line]:
|
|
930
|
+
source_lines += 1
|
|
931
|
+
elif has_directive[line]:
|
|
932
|
+
continue
|
|
933
|
+
elif has_comment[line]:
|
|
934
|
+
comment_only_lines += 1
|
|
935
|
+
else:
|
|
936
|
+
blank_lines += 1
|
|
937
|
+
lines = LineMetrics(
|
|
938
|
+
total_lines=line_count,
|
|
939
|
+
source_lines=source_lines,
|
|
940
|
+
blank_lines=blank_lines,
|
|
941
|
+
comment_only_lines=comment_only_lines,
|
|
942
|
+
comment_lines=sum(has_comment),
|
|
943
|
+
directive_lines=sum(has_directive),
|
|
944
|
+
)
|
|
945
|
+
return _ScanResult(
|
|
946
|
+
lines=lines,
|
|
947
|
+
operator_vocabulary=frozenset(operators),
|
|
948
|
+
operand_vocabulary=frozenset(operands),
|
|
949
|
+
total_operators=total_operators,
|
|
950
|
+
total_operands=total_operands,
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _scan_source_lines(
|
|
955
|
+
source: str,
|
|
956
|
+
*,
|
|
957
|
+
token_chunks: list[tuple[int, int]] | None = None,
|
|
958
|
+
) -> _ScanResult:
|
|
959
|
+
line_count = len(source.splitlines()) if source else 0
|
|
960
|
+
line_states = bytearray(line_count)
|
|
961
|
+
current_line = 0
|
|
962
|
+
|
|
963
|
+
def mark_span(flag: int, start: int, end: int) -> None:
|
|
964
|
+
nonlocal current_line
|
|
965
|
+
if not line_states or end <= start:
|
|
966
|
+
return
|
|
967
|
+
newline_count = _line_break_count(source, start, end)
|
|
968
|
+
end_line = current_line + newline_count
|
|
969
|
+
if source[end - 1] in {"\r", "\n"}:
|
|
970
|
+
end_line -= 1
|
|
971
|
+
for line in range(current_line, min(end_line, len(line_states) - 1) + 1):
|
|
972
|
+
line_states[line] |= flag
|
|
973
|
+
current_line += newline_count
|
|
974
|
+
|
|
975
|
+
index = 0
|
|
976
|
+
length = len(source)
|
|
977
|
+
chunk_start = 0
|
|
978
|
+
while index < length:
|
|
979
|
+
match = _LINE_SCAN_SPECIAL_RE.search(source, index)
|
|
980
|
+
special_start = match.start() if match is not None else length
|
|
981
|
+
if (
|
|
982
|
+
current_line < line_count
|
|
983
|
+
and _NON_WHITESPACE_RE.search(source, index, special_start) is not None
|
|
984
|
+
):
|
|
985
|
+
line_states[current_line] |= 1
|
|
986
|
+
if match is None:
|
|
987
|
+
break
|
|
988
|
+
marker = match.group()
|
|
989
|
+
index = special_start
|
|
990
|
+
if marker in {"\r", "\n", "\r\n"}:
|
|
991
|
+
current_line += 1
|
|
992
|
+
index = match.end()
|
|
993
|
+
if (
|
|
994
|
+
token_chunks is not None
|
|
995
|
+
and index - chunk_start >= _METRIC_TOKEN_CHUNK_TARGET
|
|
996
|
+
):
|
|
997
|
+
token_chunks.append((chunk_start, index))
|
|
998
|
+
chunk_start = index
|
|
999
|
+
continue
|
|
1000
|
+
if marker == "//":
|
|
1001
|
+
end = _line_ending_start(source, index + 2)
|
|
1002
|
+
mark_span(2, index, end)
|
|
1003
|
+
index = end
|
|
1004
|
+
continue
|
|
1005
|
+
if marker == "{":
|
|
1006
|
+
close = source.find("}", index + 1)
|
|
1007
|
+
end = length if close < 0 else close + 1
|
|
1008
|
+
directive = index + 1 < length and source[index + 1] == "$"
|
|
1009
|
+
mark_span(4 if directive else 2, index, end)
|
|
1010
|
+
index = end
|
|
1011
|
+
continue
|
|
1012
|
+
if marker == "(*":
|
|
1013
|
+
close = source.find("*)", index + 2)
|
|
1014
|
+
end = length if close < 0 else close + 2
|
|
1015
|
+
directive = index + 2 < length and source[index + 2] == "$"
|
|
1016
|
+
mark_span(4 if directive else 2, index, end)
|
|
1017
|
+
index = end
|
|
1018
|
+
continue
|
|
1019
|
+
end = _quoted_end(source, index)
|
|
1020
|
+
mark_span(1, index, end)
|
|
1021
|
+
index = end
|
|
1022
|
+
|
|
1023
|
+
if token_chunks is not None and chunk_start < length:
|
|
1024
|
+
token_chunks.append((chunk_start, length))
|
|
1025
|
+
|
|
1026
|
+
source_lines = 0
|
|
1027
|
+
blank_lines = 0
|
|
1028
|
+
comment_only_lines = 0
|
|
1029
|
+
for state in line_states:
|
|
1030
|
+
if state & 1:
|
|
1031
|
+
source_lines += 1
|
|
1032
|
+
elif state & 4:
|
|
1033
|
+
continue
|
|
1034
|
+
elif state & 2:
|
|
1035
|
+
comment_only_lines += 1
|
|
1036
|
+
else:
|
|
1037
|
+
blank_lines += 1
|
|
1038
|
+
return _ScanResult(
|
|
1039
|
+
lines=LineMetrics(
|
|
1040
|
+
total_lines=line_count,
|
|
1041
|
+
source_lines=source_lines,
|
|
1042
|
+
blank_lines=blank_lines,
|
|
1043
|
+
comment_only_lines=comment_only_lines,
|
|
1044
|
+
comment_lines=sum(bool(state & 2) for state in line_states),
|
|
1045
|
+
directive_lines=sum(bool(state & 4) for state in line_states),
|
|
1046
|
+
),
|
|
1047
|
+
operator_vocabulary=frozenset(),
|
|
1048
|
+
operand_vocabulary=frozenset(),
|
|
1049
|
+
total_operators=0,
|
|
1050
|
+
total_operands=0,
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _line_break_count(source: str, start: int, end: int) -> int:
|
|
1055
|
+
return (
|
|
1056
|
+
source.count("\r", start, end)
|
|
1057
|
+
+ source.count("\n", start, end)
|
|
1058
|
+
- source.count("\r\n", start, end)
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _line_ending_start(source: str, start: int) -> int:
|
|
1063
|
+
carriage_return = source.find("\r", start)
|
|
1064
|
+
line_feed = source.find("\n", start)
|
|
1065
|
+
candidates = [
|
|
1066
|
+
position
|
|
1067
|
+
for position in (carriage_return, line_feed)
|
|
1068
|
+
if position >= 0
|
|
1069
|
+
]
|
|
1070
|
+
return min(candidates, default=len(source))
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _quoted_end(source: str, start: int) -> int:
|
|
1074
|
+
index = start + 1
|
|
1075
|
+
while index < len(source):
|
|
1076
|
+
if source[index] != "'":
|
|
1077
|
+
index += 1
|
|
1078
|
+
continue
|
|
1079
|
+
if index + 1 < len(source) and source[index + 1] == "'":
|
|
1080
|
+
index += 2
|
|
1081
|
+
continue
|
|
1082
|
+
return index + 1
|
|
1083
|
+
return len(source)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _number_end(source: str, start: int) -> int:
|
|
1087
|
+
index = start + 1
|
|
1088
|
+
while index < len(source):
|
|
1089
|
+
character = source[index]
|
|
1090
|
+
if character.isalnum() or character in {"_", "."}:
|
|
1091
|
+
if source.startswith("..", index):
|
|
1092
|
+
break
|
|
1093
|
+
index += 1
|
|
1094
|
+
continue
|
|
1095
|
+
if character in {"+", "-"} and index > start and source[index - 1].casefold() == "e":
|
|
1096
|
+
index += 1
|
|
1097
|
+
continue
|
|
1098
|
+
break
|
|
1099
|
+
return index
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def _identifier_start(character: str) -> bool:
|
|
1103
|
+
return character == "_" or character.isalpha()
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def _identifier_part(character: str) -> bool:
|
|
1107
|
+
return character == "_" or character.isalnum()
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _walk(node: SyntaxNode) -> Iterable[SyntaxNode]:
|
|
1111
|
+
pending = [node]
|
|
1112
|
+
while pending:
|
|
1113
|
+
current = pending.pop()
|
|
1114
|
+
yield current
|
|
1115
|
+
pending.extend(reversed(current.child_nodes))
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _dependencies(root: SyntaxNode) -> tuple[str, ...]:
|
|
1119
|
+
dependencies: dict[str, str] = {}
|
|
1120
|
+
for node in _walk(root):
|
|
1121
|
+
if node.typ not in _DEPENDENCY_NODES:
|
|
1122
|
+
continue
|
|
1123
|
+
for child in node.child_nodes:
|
|
1124
|
+
if child.typ != SyntaxNodeType.ntUnit:
|
|
1125
|
+
continue
|
|
1126
|
+
name = child.get_attribute(AttributeName.anName).strip()
|
|
1127
|
+
if name:
|
|
1128
|
+
dependencies.setdefault(name.casefold(), name)
|
|
1129
|
+
return tuple(sorted(dependencies.values(), key=lambda value: (value.casefold(), value)))
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _cyclomatic_metrics(root: SyntaxNode) -> CyclomaticMetrics:
|
|
1133
|
+
routines: list[RoutineComplexity] = []
|
|
1134
|
+
for node in _walk(root):
|
|
1135
|
+
if node.typ != SyntaxNodeType.ntMethod:
|
|
1136
|
+
continue
|
|
1137
|
+
if not any(child.typ == SyntaxNodeType.ntStatements for child in node.child_nodes):
|
|
1138
|
+
continue
|
|
1139
|
+
name = node.get_attribute(AttributeName.anName) or f"anonymous@{node.line}"
|
|
1140
|
+
routines.append(
|
|
1141
|
+
RoutineComplexity(
|
|
1142
|
+
name=name,
|
|
1143
|
+
value=1 + _decision_count(node),
|
|
1144
|
+
line=node.line,
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
return CyclomaticMetrics.from_routines(routines)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _decision_count(node: SyntaxNode) -> int:
|
|
1151
|
+
count = 0
|
|
1152
|
+
pending = list(reversed(node.child_nodes))
|
|
1153
|
+
while pending:
|
|
1154
|
+
child = pending.pop()
|
|
1155
|
+
if child.typ == SyntaxNodeType.ntMethod:
|
|
1156
|
+
continue
|
|
1157
|
+
if child.typ in _DECISION_NODES:
|
|
1158
|
+
count += 1
|
|
1159
|
+
if child.typ == SyntaxNodeType.ntExcept and not any(
|
|
1160
|
+
descendant.typ == SyntaxNodeType.ntExceptionHandler
|
|
1161
|
+
for descendant in child.child_nodes
|
|
1162
|
+
):
|
|
1163
|
+
count += 1
|
|
1164
|
+
pending.extend(reversed(child.child_nodes))
|
|
1165
|
+
return count
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def _syntax_symbol_counts(root: SyntaxNode) -> Counter[str]:
|
|
1169
|
+
counts: Counter[str] = Counter()
|
|
1170
|
+
pending = [root]
|
|
1171
|
+
while pending:
|
|
1172
|
+
node = pending.pop()
|
|
1173
|
+
if node.typ == SyntaxNodeType.ntUses:
|
|
1174
|
+
continue
|
|
1175
|
+
if node.typ == SyntaxNodeType.ntTypeSection:
|
|
1176
|
+
for child in node.child_nodes:
|
|
1177
|
+
if child.typ == SyntaxNodeType.ntTypeDecl:
|
|
1178
|
+
_count_syntax_type_declaration(child, counts, pending)
|
|
1179
|
+
continue
|
|
1180
|
+
if node.typ == SyntaxNodeType.ntVariables:
|
|
1181
|
+
_count_named_children(
|
|
1182
|
+
node,
|
|
1183
|
+
SyntaxNodeType.ntVariable,
|
|
1184
|
+
SymbolKind.VARIABLE,
|
|
1185
|
+
counts,
|
|
1186
|
+
)
|
|
1187
|
+
continue
|
|
1188
|
+
if node.typ == SyntaxNodeType.ntVariable:
|
|
1189
|
+
_count_named_declaration(node, SymbolKind.VARIABLE, counts)
|
|
1190
|
+
continue
|
|
1191
|
+
if node.typ == SyntaxNodeType.ntConstants:
|
|
1192
|
+
_count_named_children(
|
|
1193
|
+
node,
|
|
1194
|
+
SyntaxNodeType.ntConstant,
|
|
1195
|
+
SymbolKind.CONSTANT,
|
|
1196
|
+
counts,
|
|
1197
|
+
)
|
|
1198
|
+
continue
|
|
1199
|
+
if node.typ == SyntaxNodeType.ntMethod:
|
|
1200
|
+
_count_syntax_method(node, counts, pending)
|
|
1201
|
+
continue
|
|
1202
|
+
if node.typ == SyntaxNodeType.ntProperty:
|
|
1203
|
+
_count_named_declaration(node, SymbolKind.PROPERTY, counts)
|
|
1204
|
+
continue
|
|
1205
|
+
if node.typ == SyntaxNodeType.ntField:
|
|
1206
|
+
_count_named_declaration(node, SymbolKind.FIELD, counts)
|
|
1207
|
+
continue
|
|
1208
|
+
if node.typ == SyntaxNodeType.ntLabel:
|
|
1209
|
+
_count_named_declaration(node, SymbolKind.LABEL, counts)
|
|
1210
|
+
continue
|
|
1211
|
+
pending.extend(reversed(node.child_nodes))
|
|
1212
|
+
return counts
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _count_syntax_type_declaration(
|
|
1216
|
+
node: SyntaxNode,
|
|
1217
|
+
counts: Counter[str],
|
|
1218
|
+
pending: list[SyntaxNode],
|
|
1219
|
+
) -> None:
|
|
1220
|
+
if not _syntax_declaration_name(node):
|
|
1221
|
+
return
|
|
1222
|
+
type_node = node.find_node(SyntaxNodeType.ntType)
|
|
1223
|
+
counts[_syntax_type_symbol_kind(node).value] += 1
|
|
1224
|
+
type_parameters = node.find_node(SyntaxNodeType.ntTypeParams)
|
|
1225
|
+
if type_parameters is not None:
|
|
1226
|
+
_count_named_children(
|
|
1227
|
+
type_parameters,
|
|
1228
|
+
SyntaxNodeType.ntTypeParam,
|
|
1229
|
+
SymbolKind.TYPE_PARAMETER,
|
|
1230
|
+
counts,
|
|
1231
|
+
)
|
|
1232
|
+
if type_node is None:
|
|
1233
|
+
return
|
|
1234
|
+
if type_node.get_attribute(AttributeName.anName) == "enum":
|
|
1235
|
+
_count_named_children(
|
|
1236
|
+
type_node,
|
|
1237
|
+
SyntaxNodeType.ntEnum,
|
|
1238
|
+
SymbolKind.ENUM_VALUE,
|
|
1239
|
+
counts,
|
|
1240
|
+
)
|
|
1241
|
+
pending.extend(reversed(type_node.child_nodes))
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def _count_syntax_method(
|
|
1245
|
+
node: SyntaxNode,
|
|
1246
|
+
counts: Counter[str],
|
|
1247
|
+
pending: list[SyntaxNode],
|
|
1248
|
+
) -> None:
|
|
1249
|
+
if not _syntax_declaration_name(node):
|
|
1250
|
+
return
|
|
1251
|
+
counts[_syntax_method_symbol_kind(node).value] += 1
|
|
1252
|
+
parameters = node.find_node(SyntaxNodeType.ntParameters)
|
|
1253
|
+
if parameters is not None:
|
|
1254
|
+
_count_named_children(
|
|
1255
|
+
parameters,
|
|
1256
|
+
SyntaxNodeType.ntParameter,
|
|
1257
|
+
SymbolKind.PARAMETER,
|
|
1258
|
+
counts,
|
|
1259
|
+
)
|
|
1260
|
+
type_parameters = node.find_node(SyntaxNodeType.ntTypeParams)
|
|
1261
|
+
if type_parameters is not None:
|
|
1262
|
+
_count_named_children(
|
|
1263
|
+
type_parameters,
|
|
1264
|
+
SyntaxNodeType.ntTypeParam,
|
|
1265
|
+
SymbolKind.TYPE_PARAMETER,
|
|
1266
|
+
counts,
|
|
1267
|
+
)
|
|
1268
|
+
excluded = {
|
|
1269
|
+
SyntaxNodeType.ntParameters,
|
|
1270
|
+
SyntaxNodeType.ntReturnType,
|
|
1271
|
+
SyntaxNodeType.ntTypeParams,
|
|
1272
|
+
}
|
|
1273
|
+
pending.extend(
|
|
1274
|
+
child
|
|
1275
|
+
for child in reversed(node.child_nodes)
|
|
1276
|
+
if child.typ not in excluded
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def _count_named_children(
|
|
1281
|
+
node: SyntaxNode,
|
|
1282
|
+
child_type: SyntaxNodeType,
|
|
1283
|
+
kind: SymbolKind,
|
|
1284
|
+
counts: Counter[str],
|
|
1285
|
+
) -> None:
|
|
1286
|
+
for child in node.child_nodes:
|
|
1287
|
+
if child.typ == child_type:
|
|
1288
|
+
_count_named_declaration(child, kind, counts)
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def _count_named_declaration(
|
|
1292
|
+
node: SyntaxNode,
|
|
1293
|
+
kind: SymbolKind,
|
|
1294
|
+
counts: Counter[str],
|
|
1295
|
+
) -> None:
|
|
1296
|
+
if _syntax_declaration_name(node):
|
|
1297
|
+
counts[kind.value] += 1
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _syntax_type_symbol_kind(node: SyntaxNode) -> SymbolKind:
|
|
1301
|
+
type_node = node.find_node(SyntaxNodeType.ntType)
|
|
1302
|
+
if type_node is None:
|
|
1303
|
+
return SymbolKind.TYPE
|
|
1304
|
+
type_tag = type_node.get_attribute(AttributeName.anType)
|
|
1305
|
+
name_tag = type_node.get_attribute(AttributeName.anName)
|
|
1306
|
+
if type_tag == "class":
|
|
1307
|
+
return SymbolKind.CLASS
|
|
1308
|
+
if type_tag == "record":
|
|
1309
|
+
return SymbolKind.RECORD
|
|
1310
|
+
if type_tag in {"interface", "dispinterface"}:
|
|
1311
|
+
return SymbolKind.INTERFACE
|
|
1312
|
+
if name_tag == "enum":
|
|
1313
|
+
return SymbolKind.ENUM
|
|
1314
|
+
return SymbolKind.TYPE
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def _syntax_method_symbol_kind(node: SyntaxNode) -> SymbolKind:
|
|
1318
|
+
return {
|
|
1319
|
+
"function": SymbolKind.FUNCTION,
|
|
1320
|
+
"procedure": SymbolKind.PROCEDURE,
|
|
1321
|
+
"constructor": SymbolKind.CONSTRUCTOR,
|
|
1322
|
+
"destructor": SymbolKind.DESTRUCTOR,
|
|
1323
|
+
}.get(node.get_attribute(AttributeName.anKind), SymbolKind.METHOD)
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def _syntax_declaration_name(node: SyntaxNode) -> str:
|
|
1327
|
+
if isinstance(node, ValuedSyntaxNode):
|
|
1328
|
+
return node.value
|
|
1329
|
+
name = node.get_attribute(AttributeName.anName)
|
|
1330
|
+
if name:
|
|
1331
|
+
return name
|
|
1332
|
+
for child in node.child_nodes:
|
|
1333
|
+
if child.typ != SyntaxNodeType.ntName:
|
|
1334
|
+
continue
|
|
1335
|
+
if isinstance(child, ValuedSyntaxNode):
|
|
1336
|
+
return child.value
|
|
1337
|
+
return child.get_attribute(AttributeName.anName)
|
|
1338
|
+
return ""
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _abstract_type_counts(root: SyntaxNode) -> tuple[int, int]:
|
|
1342
|
+
class_like = 0
|
|
1343
|
+
abstract = 0
|
|
1344
|
+
for node in _walk(root):
|
|
1345
|
+
if node.typ != SyntaxNodeType.ntTypeDecl:
|
|
1346
|
+
continue
|
|
1347
|
+
type_node = next(
|
|
1348
|
+
(child for child in node.child_nodes if child.typ == SyntaxNodeType.ntType),
|
|
1349
|
+
None,
|
|
1350
|
+
)
|
|
1351
|
+
if type_node is None:
|
|
1352
|
+
continue
|
|
1353
|
+
kind = type_node.get_attribute(AttributeName.anType).casefold()
|
|
1354
|
+
if kind not in {"class", "interface", "dispinterface"}:
|
|
1355
|
+
continue
|
|
1356
|
+
class_like += 1
|
|
1357
|
+
has_abstract_method = any(
|
|
1358
|
+
child.typ == SyntaxNodeType.ntMethod
|
|
1359
|
+
and child.get_attribute(AttributeName.anAbstract) == "true"
|
|
1360
|
+
for child in _walk(type_node)
|
|
1361
|
+
)
|
|
1362
|
+
if kind in {"interface", "dispinterface"} or (
|
|
1363
|
+
type_node.get_attribute(AttributeName.anAbstract) == "true"
|
|
1364
|
+
or has_abstract_method
|
|
1365
|
+
):
|
|
1366
|
+
abstract += 1
|
|
1367
|
+
return class_like, abstract
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def _halstead_metrics(
|
|
1371
|
+
*,
|
|
1372
|
+
operator_vocabulary: frozenset[str],
|
|
1373
|
+
operand_vocabulary: frozenset[str],
|
|
1374
|
+
total_operators: int,
|
|
1375
|
+
total_operands: int,
|
|
1376
|
+
) -> HalsteadMetrics:
|
|
1377
|
+
distinct_operators = len(operator_vocabulary)
|
|
1378
|
+
distinct_operands = len(operand_vocabulary)
|
|
1379
|
+
vocabulary = distinct_operators + distinct_operands
|
|
1380
|
+
length = total_operators + total_operands
|
|
1381
|
+
calculated_length = _halstead_term(distinct_operators) + _halstead_term(distinct_operands)
|
|
1382
|
+
volume = length * math.log2(vocabulary) if vocabulary > 1 else 0.0
|
|
1383
|
+
difficulty = (
|
|
1384
|
+
(distinct_operators / 2.0) * (total_operands / distinct_operands)
|
|
1385
|
+
if distinct_operands
|
|
1386
|
+
else 0.0
|
|
1387
|
+
)
|
|
1388
|
+
effort = difficulty * volume
|
|
1389
|
+
return HalsteadMetrics(
|
|
1390
|
+
distinct_operators=distinct_operators,
|
|
1391
|
+
distinct_operands=distinct_operands,
|
|
1392
|
+
total_operators=total_operators,
|
|
1393
|
+
total_operands=total_operands,
|
|
1394
|
+
vocabulary=vocabulary,
|
|
1395
|
+
length=length,
|
|
1396
|
+
calculated_length=calculated_length,
|
|
1397
|
+
volume=volume,
|
|
1398
|
+
difficulty=difficulty,
|
|
1399
|
+
effort=effort,
|
|
1400
|
+
estimated_time_seconds=effort / 18.0,
|
|
1401
|
+
estimated_defects=volume / 3000.0,
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def _halstead_term(value: int) -> float:
|
|
1406
|
+
return value * math.log2(value) if value > 1 else 0.0
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _maintainability_index(
|
|
1410
|
+
*,
|
|
1411
|
+
volume: float,
|
|
1412
|
+
complexity: int,
|
|
1413
|
+
source_lines: int,
|
|
1414
|
+
routine_count: int,
|
|
1415
|
+
) -> float:
|
|
1416
|
+
if source_lines == 0:
|
|
1417
|
+
return 100.0
|
|
1418
|
+
divisor = max(1, routine_count)
|
|
1419
|
+
raw = (
|
|
1420
|
+
171.0
|
|
1421
|
+
- 5.2 * math.log10(max(volume / divisor, 1.0))
|
|
1422
|
+
- 0.23 * complexity / divisor
|
|
1423
|
+
- 16.2 * math.log10(max(source_lines / divisor, 1.0))
|
|
1424
|
+
) * 100.0 / 171.0
|
|
1425
|
+
return min(100.0, max(0.0, raw))
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def _sum_line_metrics(metrics: Iterable[LineMetrics]) -> LineMetrics:
|
|
1429
|
+
items = tuple(metrics)
|
|
1430
|
+
return LineMetrics(
|
|
1431
|
+
total_lines=sum(item.total_lines for item in items),
|
|
1432
|
+
source_lines=sum(item.source_lines for item in items),
|
|
1433
|
+
blank_lines=sum(item.blank_lines for item in items),
|
|
1434
|
+
comment_only_lines=sum(item.comment_only_lines for item in items),
|
|
1435
|
+
comment_lines=sum(item.comment_lines for item in items),
|
|
1436
|
+
directive_lines=sum(item.directive_lines for item in items),
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
__all__ = [
|
|
1441
|
+
"CyclomaticMetrics",
|
|
1442
|
+
"HalsteadMetrics",
|
|
1443
|
+
"LineMetrics",
|
|
1444
|
+
"MetricProblem",
|
|
1445
|
+
"ProjectMetrics",
|
|
1446
|
+
"RoutineComplexity",
|
|
1447
|
+
"UnitMetrics",
|
|
1448
|
+
"aggregate_project_metrics",
|
|
1449
|
+
"analyze_include",
|
|
1450
|
+
"analyze_project",
|
|
1451
|
+
"analyze_unit",
|
|
1452
|
+
]
|