@untangle-code/complexity 0.0.0-stage → 0.1.1
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.
- package/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +235 -2
- package/THIRD_PARTY_LICENSES.md +24 -0
- package/dist/aggregator.d.ts +28 -0
- package/dist/aggregator.d.ts.map +1 -0
- package/dist/aggregator.js +77 -0
- package/dist/aggregator.js.map +1 -0
- package/dist/assemble.d.ts +50 -0
- package/dist/assemble.d.ts.map +1 -0
- package/dist/assemble.js +43 -0
- package/dist/assemble.js.map +1 -0
- package/dist/cognitive.d.ts +62 -0
- package/dist/cognitive.d.ts.map +1 -0
- package/dist/cognitive.js +62 -0
- package/dist/cognitive.js.map +1 -0
- package/dist/cyclomatic.d.ts +29 -0
- package/dist/cyclomatic.d.ts.map +1 -0
- package/dist/cyclomatic.js +52 -0
- package/dist/cyclomatic.js.map +1 -0
- package/dist/entropy.d.ts +5 -0
- package/dist/entropy.d.ts.map +1 -0
- package/dist/entropy.js +29 -0
- package/dist/entropy.js.map +1 -0
- package/dist/explain.d.ts +22 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +35 -0
- package/dist/explain.js.map +1 -0
- package/dist/halstead.d.ts +23 -0
- package/dist/halstead.d.ts.map +1 -0
- package/dist/halstead.js +24 -0
- package/dist/halstead.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/build-ir.d.ts +105 -0
- package/dist/ir/build-ir.d.ts.map +1 -0
- package/dist/ir/build-ir.js +267 -0
- package/dist/ir/build-ir.js.map +1 -0
- package/dist/maintainability.d.ts +15 -0
- package/dist/maintainability.d.ts.map +1 -0
- package/dist/maintainability.js +10 -0
- package/dist/maintainability.js.map +1 -0
- package/dist/node-kind-heuristics.d.ts +24 -0
- package/dist/node-kind-heuristics.d.ts.map +1 -0
- package/dist/node-kind-heuristics.js +37 -0
- package/dist/node-kind-heuristics.js.map +1 -0
- package/dist/prose/metrics.d.ts +81 -0
- package/dist/prose/metrics.d.ts.map +1 -0
- package/dist/prose/metrics.js +103 -0
- package/dist/prose/metrics.js.map +1 -0
- package/dist/scope-aggregate.d.ts +229 -0
- package/dist/scope-aggregate.d.ts.map +1 -0
- package/dist/scope-aggregate.js +588 -0
- package/dist/scope-aggregate.js.map +1 -0
- package/dist/scope-identity.d.ts +85 -0
- package/dist/scope-identity.d.ts.map +1 -0
- package/dist/scope-identity.js +144 -0
- package/dist/scope-identity.js.map +1 -0
- package/dist/scope.d.ts +36 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +60 -0
- package/dist/scope.js.map +1 -0
- package/dist/structural.d.ts +30 -0
- package/dist/structural.d.ts.map +1 -0
- package/dist/structural.js +98 -0
- package/dist/structural.js.map +1 -0
- package/dist/syntactic.d.ts +35 -0
- package/dist/syntactic.d.ts.map +1 -0
- package/dist/syntactic.js +143 -0
- package/dist/syntactic.js.map +1 -0
- package/dist/text-lines.d.ts +34 -0
- package/dist/text-lines.d.ts.map +1 -0
- package/dist/text-lines.js +60 -0
- package/dist/text-lines.js.map +1 -0
- package/dist/textual.d.ts +28 -0
- package/dist/textual.d.ts.map +1 -0
- package/dist/textual.js +75 -0
- package/dist/textual.js.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -3
- package/src/aggregator.ts +105 -0
- package/src/assemble.ts +109 -0
- package/src/cognitive.ts +127 -0
- package/src/cyclomatic.ts +78 -0
- package/src/entropy.ts +30 -0
- package/src/explain.ts +64 -0
- package/src/halstead.ts +54 -0
- package/src/index.ts +85 -0
- package/src/ir/build-ir.ts +411 -0
- package/src/maintainability.ts +29 -0
- package/src/node-kind-heuristics.ts +55 -0
- package/src/prose/metrics.ts +201 -0
- package/src/scope-aggregate.ts +775 -0
- package/src/scope-identity.ts +240 -0
- package/src/scope.ts +77 -0
- package/src/structural.ts +132 -0
- package/src/syntactic.ts +183 -0
- package/src/text-lines.ts +90 -0
- package/src/textual.ts +119 -0
- package/src/types.ts +121 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR producer: the bridge `ComplexityFacts` -> `ComplexityIRNode`
|
|
3
|
+
* (architecture.md §6.7, spec.md §16.4).
|
|
4
|
+
*
|
|
5
|
+
* architecture.md §6.7 says "Sprachadapter uebersetzen AST-Nodes in diese
|
|
6
|
+
* IR", but the `LanguageAdapter` port (domain `analysis.ts`) deliberately
|
|
7
|
+
* has no IR method — adapters emit flat, language-neutral `ComplexityFacts`.
|
|
8
|
+
* This module closes that gap INSIDE the complexity package: it rebuilds the
|
|
9
|
+
* `ComplexityIRNode` tree `computeCognitive` (and the branching metrics of
|
|
10
|
+
* `computeSyntactic`) require from the byte-ranges the facts already carry,
|
|
11
|
+
* so no language adapter needs to know the IR shape at all.
|
|
12
|
+
*
|
|
13
|
+
* INPUT CONTRACT (facts, as produced by parser-registry's single walk):
|
|
14
|
+
* - every `DecisionFact`/`NestingFact` range is the source range of the AST
|
|
15
|
+
* node that caused it; ranges of distinct facts never partially overlap
|
|
16
|
+
* (they are AST node ranges: equal, disjoint, or fully nested);
|
|
17
|
+
* - an `if`/`loop`/`catch`/`ternary` emits BOTH a `DecisionFact` and a
|
|
18
|
+
* `NestingFact` with the identical range (they are merged into one IR
|
|
19
|
+
* node here);
|
|
20
|
+
* - an `else if` is a `DecisionFact` of kind `"else_if"` plus a
|
|
21
|
+
* `NestingFact` with `continuation: true`, and its range lies INSIDE the
|
|
22
|
+
* range of the `if` it continues (the raw AST "alternate" shape).
|
|
23
|
+
*
|
|
24
|
+
* OUTPUT CONTRACT (what `computeCognitive` requires — see cognitive.ts):
|
|
25
|
+
* - an `if` / `else if` / `else if` chain becomes a FLAT run of sibling IR
|
|
26
|
+
* nodes under the parent of the first `if`, NEVER the raw AST's nested
|
|
27
|
+
* alternate shape. This is what makes "`else if` erhaelt keinen
|
|
28
|
+
* vollstaendigen neuen Nesting-Aufschlag" (spec.md §16.4) fall out of the
|
|
29
|
+
* traversal: siblings share the ambient depth. Implemented via the
|
|
30
|
+
* attachment rule `finalParent(continuationNode) =
|
|
31
|
+
* finalParent(itsContainmentParent)`, which flattens arbitrarily long
|
|
32
|
+
* chains while keeping a genuinely nested `if` (inside another `if`'s
|
|
33
|
+
* BODY) at its deeper level;
|
|
34
|
+
* - each branch keeps its own consequent content as `children` (content
|
|
35
|
+
* ranges nest inside the branch's own range, so containment placement
|
|
36
|
+
* already produces that).
|
|
37
|
+
*
|
|
38
|
+
* Documented mapping decisions (Untangle decisions, mirrored in the README):
|
|
39
|
+
* - nesting constructs map to IR kinds: if->"decision", loop->"loop",
|
|
40
|
+
* catch->"catch", ternary->"ternary", switch->"switch", lambda->"lambda";
|
|
41
|
+
* all of them are nesting boundaries (`nestingBoundary: true`), matching
|
|
42
|
+
* the boundary list documented in cognitive.ts;
|
|
43
|
+
* - a `case` decision (no nesting fact of its own) becomes a `"case"` node
|
|
44
|
+
* that is ALSO a nesting boundary — case bodies are one level deeper than
|
|
45
|
+
* the switch head, per cognitive.ts's documented boundary list;
|
|
46
|
+
* - logical-operator decisions (`logical_and`/`logical_or`/`short_circuit`)
|
|
47
|
+
* become non-boundary `"logical_sequence"` nodes carrying their
|
|
48
|
+
* `DecisionFact` (grouping happens in the consumers via `logicalGroupId`);
|
|
49
|
+
* - CONDITION HOISTING: a logical node whose direct IR parent is a
|
|
50
|
+
* condition-bearing boundary (`decision`/`loop`/`ternary`/`switch`) is
|
|
51
|
+
* hoisted to a sibling placed immediately BEFORE that boundary node. This
|
|
52
|
+
* encodes the untangle-cognitive-v1 convention that a guard's condition
|
|
53
|
+
* operators score at the guard's own ambient depth (see the golden case in
|
|
54
|
+
* cognitive.test.ts), and is exact for `switch` (everything nested in a
|
|
55
|
+
* switch body sits inside a `case` node). For `if`/loops/ternaries it is a
|
|
56
|
+
* documented approximation: facts carry no condition/body split, so a
|
|
57
|
+
* logical group written as a DIRECT body statement of the guard (e.g.
|
|
58
|
+
* `if (x) y = a && b;`) is hoisted too and scores one nesting level lower
|
|
59
|
+
* than a hypothetical exact split would give. Making this exact needs a
|
|
60
|
+
* `conditionRange` on `DecisionFact` (or a `bodyRange` on `NestingFact`)
|
|
61
|
+
* from the adapters — tracked as an open issue, not guessed here;
|
|
62
|
+
* - every descendant symbol scope of the target symbol is included: its
|
|
63
|
+
* facts participate in the target's IR, and the scope itself becomes a
|
|
64
|
+
* `"lambda"` boundary node when it is function-like (function / method /
|
|
65
|
+
* constructor — deduplicated by range against the adapter's own `lambda`
|
|
66
|
+
* nesting events) or a transparent non-boundary `"block"` node otherwise
|
|
67
|
+
* (class/interface/enum/namespace/type/property). This is the "documented
|
|
68
|
+
* nesting context" spec.md §16.4 demands for local functions/lambdas: a
|
|
69
|
+
* symbol's cognitive value includes the control flow of the lambdas it
|
|
70
|
+
* defines, one nesting level deeper — while those lambdas still get their
|
|
71
|
+
* own, separately-scoped snapshot when measured directly;
|
|
72
|
+
* - recursion (spec.md §16.4 "Rekursion wird markiert und KANN einen
|
|
73
|
+
* Beitrag erzeugen"): when the caller supplies the file's `RawCall`s
|
|
74
|
+
* (adapters expose them via `extractCalls`, but they are not part of
|
|
75
|
+
* `ComplexityFacts`), a call site inside the subtree whose unqualified
|
|
76
|
+
* callee name (or `this.`-qualified name, for methods) equals the name of
|
|
77
|
+
* its OWN enclosing scope becomes a `"recursion_marker"` node — direct
|
|
78
|
+
* self-recursion only; mutual recursion needs the call graph and is out of
|
|
79
|
+
* scope here.
|
|
80
|
+
*/
|
|
81
|
+
import type {
|
|
82
|
+
ComplexityFacts,
|
|
83
|
+
ComplexityIRNode,
|
|
84
|
+
DecisionFact,
|
|
85
|
+
IRKind,
|
|
86
|
+
RawCall,
|
|
87
|
+
SymbolScopeFact,
|
|
88
|
+
} from "@untangle-code/domain";
|
|
89
|
+
|
|
90
|
+
import { containsRange, rangeKey } from "../scope.js";
|
|
91
|
+
|
|
92
|
+
export interface BuildSymbolIROptions {
|
|
93
|
+
/**
|
|
94
|
+
* Raw call sites of the same file (from `LanguageAdapter.extractCalls`).
|
|
95
|
+
* When given, direct self-recursion is marked with `"recursion_marker"`
|
|
96
|
+
* nodes; when omitted, no recursion contributions are produced (allowed:
|
|
97
|
+
* spec.md §16.4 makes the recursion contribution optional).
|
|
98
|
+
*/
|
|
99
|
+
calls?: readonly RawCall[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const FUNCTION_LIKE_SCOPE_KINDS: ReadonlySet<string> = new Set([
|
|
103
|
+
"function",
|
|
104
|
+
"method",
|
|
105
|
+
"constructor",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const LOGICAL_DECISION_KINDS: ReadonlySet<string> = new Set([
|
|
109
|
+
"logical_and",
|
|
110
|
+
"logical_or",
|
|
111
|
+
"short_circuit",
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
/** Boundary IR kinds whose head expression (condition) can carry logical groups. */
|
|
115
|
+
const CONDITION_BEARING_KINDS: ReadonlySet<IRKind> = new Set([
|
|
116
|
+
"decision",
|
|
117
|
+
"loop",
|
|
118
|
+
"ternary",
|
|
119
|
+
"switch",
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
function irKindForConstruct(construct: string): IRKind {
|
|
123
|
+
switch (construct) {
|
|
124
|
+
case "if":
|
|
125
|
+
return "decision";
|
|
126
|
+
case "loop":
|
|
127
|
+
return "loop";
|
|
128
|
+
case "catch":
|
|
129
|
+
return "catch";
|
|
130
|
+
case "ternary":
|
|
131
|
+
return "ternary";
|
|
132
|
+
case "switch":
|
|
133
|
+
return "switch";
|
|
134
|
+
case "lambda":
|
|
135
|
+
return "lambda";
|
|
136
|
+
default:
|
|
137
|
+
// Unknown future constructs still nest conservatively (they were
|
|
138
|
+
// emitted as nesting events, so they ARE nesting constructs).
|
|
139
|
+
return "other";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface ProtoNode {
|
|
144
|
+
node: ComplexityIRNode;
|
|
145
|
+
/** From `NestingFact.continuation` — drives the else-if sibling flattening. */
|
|
146
|
+
continuation: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isLogicalDecisionNode(node: ComplexityIRNode): boolean {
|
|
150
|
+
return (
|
|
151
|
+
node.decision !== undefined &&
|
|
152
|
+
LOGICAL_DECISION_KINDS.has(node.decision.kind) &&
|
|
153
|
+
node.nestingBoundary !== true
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isConditionBearingBoundary(node: ComplexityIRNode): boolean {
|
|
158
|
+
return node.nestingBoundary === true && CONDITION_BEARING_KINDS.has(node.kind);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Condition hoisting (see "CONDITION HOISTING" above): a logical node that
|
|
163
|
+
* is a direct child of a condition-bearing boundary moves up EXACTLY ONE
|
|
164
|
+
* level, inserted immediately before its former parent — that places the
|
|
165
|
+
* group at the guard's own ambient depth (the guard's children sit one
|
|
166
|
+
* boundary deeper than the guard itself). Never more than one level: the
|
|
167
|
+
* sweep for `parent` runs before recursing into its children, so a node
|
|
168
|
+
* hoisted out of a grandchild boundary is not swept again by any ancestor.
|
|
169
|
+
*/
|
|
170
|
+
function hoistConditionGroups(parent: ComplexityIRNode): void {
|
|
171
|
+
const reordered: ComplexityIRNode[] = [];
|
|
172
|
+
for (const child of parent.children) {
|
|
173
|
+
if (isConditionBearingBoundary(child)) {
|
|
174
|
+
const staying: ComplexityIRNode[] = [];
|
|
175
|
+
for (const grandchild of child.children) {
|
|
176
|
+
if (isLogicalDecisionNode(grandchild)) {
|
|
177
|
+
reordered.push(grandchild);
|
|
178
|
+
} else {
|
|
179
|
+
staying.push(grandchild);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
child.children = staying;
|
|
183
|
+
}
|
|
184
|
+
reordered.push(child);
|
|
185
|
+
}
|
|
186
|
+
parent.children = reordered;
|
|
187
|
+
for (const child of parent.children) {
|
|
188
|
+
hoistConditionGroups(child);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isDirectSelfRecursion(call: RawCall, enclosingScope: SymbolScopeFact): boolean {
|
|
193
|
+
if (enclosingScope.name.length === 0) return false;
|
|
194
|
+
if (call.calleeName !== enclosingScope.name) return false;
|
|
195
|
+
// `foo()` inside function foo, or `this.foo()` inside method foo.
|
|
196
|
+
return call.qualifier === undefined || call.qualifier === "this";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Builds the `ComplexityIRNode` subtree for one symbol scope (kind
|
|
201
|
+
* `"symbol"`, `nestingBoundary` unset, traversal depth 0 — exactly the root
|
|
202
|
+
* shape `computeCognitive` expects), or the whole-file tree (kind `"root"`)
|
|
203
|
+
* when `symbolScopeId` is the file scope's id.
|
|
204
|
+
*
|
|
205
|
+
* Returns `undefined` when `symbolScopeId` does not exist in
|
|
206
|
+
* `facts.symbolScopes` (mirrors `selectSymbolFacts`' stale-id tolerance).
|
|
207
|
+
*
|
|
208
|
+
* Pass the UNFILTERED file facts, not `selectSymbolFacts` output: the
|
|
209
|
+
* builder selects the symbol's own facts AND its descendant scopes' facts
|
|
210
|
+
* itself (a symbol's lambdas nest into its IR — see the module doc).
|
|
211
|
+
*/
|
|
212
|
+
export function buildSymbolIR(
|
|
213
|
+
facts: ComplexityFacts,
|
|
214
|
+
symbolScopeId: string,
|
|
215
|
+
options: BuildSymbolIROptions = {},
|
|
216
|
+
): ComplexityIRNode | undefined {
|
|
217
|
+
const scopesById = new Map<string, SymbolScopeFact>();
|
|
218
|
+
for (const scope of facts.symbolScopes) scopesById.set(scope.symbolScopeId, scope);
|
|
219
|
+
const targetScope = scopesById.get(symbolScopeId);
|
|
220
|
+
if (targetScope === undefined) return undefined;
|
|
221
|
+
|
|
222
|
+
// Target scope plus all transitive descendants (lambdas, local functions,
|
|
223
|
+
// nested classes/methods, ...).
|
|
224
|
+
const childScopesByParent = new Map<string, SymbolScopeFact[]>();
|
|
225
|
+
for (const scope of facts.symbolScopes) {
|
|
226
|
+
if (scope.parentScopeId === undefined) continue;
|
|
227
|
+
const siblings = childScopesByParent.get(scope.parentScopeId) ?? [];
|
|
228
|
+
siblings.push(scope);
|
|
229
|
+
childScopesByParent.set(scope.parentScopeId, siblings);
|
|
230
|
+
}
|
|
231
|
+
const relevantScopeIds = new Set<string>([symbolScopeId]);
|
|
232
|
+
const descendantScopes: SymbolScopeFact[] = [];
|
|
233
|
+
const pending = [symbolScopeId];
|
|
234
|
+
while (pending.length > 0) {
|
|
235
|
+
const current = pending.pop() as string;
|
|
236
|
+
for (const child of childScopesByParent.get(current) ?? []) {
|
|
237
|
+
if (relevantScopeIds.has(child.symbolScopeId)) continue;
|
|
238
|
+
relevantScopeIds.add(child.symbolScopeId);
|
|
239
|
+
descendantScopes.push(child);
|
|
240
|
+
pending.push(child.symbolScopeId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- Proto nodes, deduplicated by range (an if/loop/catch/ternary emits
|
|
245
|
+
// a DecisionFact AND a NestingFact for the same AST node). ----
|
|
246
|
+
const protoByRange = new Map<string, ProtoNode>();
|
|
247
|
+
|
|
248
|
+
const ensureNode = (
|
|
249
|
+
key: string,
|
|
250
|
+
create: () => ComplexityIRNode,
|
|
251
|
+
continuation: boolean,
|
|
252
|
+
): ProtoNode => {
|
|
253
|
+
const existing = protoByRange.get(key);
|
|
254
|
+
if (existing !== undefined) return existing;
|
|
255
|
+
const created: ProtoNode = { node: create(), continuation };
|
|
256
|
+
protoByRange.set(key, created);
|
|
257
|
+
return created;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
for (const nesting of facts.nestingEvents) {
|
|
261
|
+
if (!relevantScopeIds.has(nesting.symbolScopeId)) continue;
|
|
262
|
+
ensureNode(
|
|
263
|
+
rangeKey(nesting.range),
|
|
264
|
+
() => ({
|
|
265
|
+
kind: irKindForConstruct(nesting.construct),
|
|
266
|
+
range: nesting.range,
|
|
267
|
+
children: [],
|
|
268
|
+
nestingBoundary: true,
|
|
269
|
+
symbolScopeId: nesting.symbolScopeId,
|
|
270
|
+
}),
|
|
271
|
+
nesting.continuation,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const decision of facts.decisions) {
|
|
276
|
+
if (!relevantScopeIds.has(decision.symbolScopeId)) continue;
|
|
277
|
+
const proto = ensureNode(rangeKey(decision.range), () => decisionOnlyNode(decision), false);
|
|
278
|
+
if (proto.node.decision === undefined) proto.node.decision = decision;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const scope of descendantScopes) {
|
|
282
|
+
const key = rangeKey(scope.range);
|
|
283
|
+
if (FUNCTION_LIKE_SCOPE_KINDS.has(scope.kind)) {
|
|
284
|
+
// Deduplicates against the adapter's own "lambda" nesting event (same
|
|
285
|
+
// AST node, same range) when the enclosing scope was executable.
|
|
286
|
+
ensureNode(
|
|
287
|
+
key,
|
|
288
|
+
() => ({
|
|
289
|
+
kind: "lambda",
|
|
290
|
+
range: scope.range,
|
|
291
|
+
children: [],
|
|
292
|
+
nestingBoundary: true,
|
|
293
|
+
symbolScopeId: scope.symbolScopeId,
|
|
294
|
+
}),
|
|
295
|
+
false,
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
ensureNode(
|
|
299
|
+
key,
|
|
300
|
+
() => ({
|
|
301
|
+
kind: "block",
|
|
302
|
+
range: scope.range,
|
|
303
|
+
children: [],
|
|
304
|
+
symbolScopeId: scope.symbolScopeId,
|
|
305
|
+
}),
|
|
306
|
+
false,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const call of options.calls ?? []) {
|
|
312
|
+
if (!relevantScopeIds.has(call.callerScopeId)) continue;
|
|
313
|
+
const enclosingScope = scopesById.get(call.callerScopeId);
|
|
314
|
+
if (enclosingScope === undefined) continue;
|
|
315
|
+
if (!isDirectSelfRecursion(call, enclosingScope)) continue;
|
|
316
|
+
ensureNode(
|
|
317
|
+
rangeKey(call.range),
|
|
318
|
+
() => ({
|
|
319
|
+
kind: "recursion_marker",
|
|
320
|
+
range: call.range,
|
|
321
|
+
children: [],
|
|
322
|
+
symbolScopeId: call.callerScopeId,
|
|
323
|
+
}),
|
|
324
|
+
false,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- Containment placement with else-if sibling flattening. ----
|
|
329
|
+
const root: ComplexityIRNode = {
|
|
330
|
+
kind: targetScope.kind === "file" ? "root" : "symbol",
|
|
331
|
+
range: targetScope.range,
|
|
332
|
+
children: [],
|
|
333
|
+
symbolScopeId,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const sorted = [...protoByRange.values()].sort(
|
|
337
|
+
(a, b) =>
|
|
338
|
+
a.node.range.startByte - b.node.range.startByte ||
|
|
339
|
+
b.node.range.endByte - a.node.range.endByte,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const containmentStack: ProtoNode[] = [];
|
|
343
|
+
// The node each proto was ATTACHED to (differs from the containment parent
|
|
344
|
+
// for continuation nodes — that difference IS the flattening).
|
|
345
|
+
const attachedTo = new Map<ProtoNode, ComplexityIRNode>();
|
|
346
|
+
|
|
347
|
+
for (const proto of sorted) {
|
|
348
|
+
if (!containsRange(root.range, proto.node.range)) continue; // defensive: stray fact outside the symbol
|
|
349
|
+
while (
|
|
350
|
+
containmentStack.length > 0 &&
|
|
351
|
+
!containsRange(
|
|
352
|
+
(containmentStack[containmentStack.length - 1] as ProtoNode).node.range,
|
|
353
|
+
proto.node.range,
|
|
354
|
+
)
|
|
355
|
+
) {
|
|
356
|
+
containmentStack.pop();
|
|
357
|
+
}
|
|
358
|
+
const containmentParent = containmentStack[containmentStack.length - 1];
|
|
359
|
+
let parent: ComplexityIRNode;
|
|
360
|
+
if (proto.continuation && containmentParent !== undefined) {
|
|
361
|
+
// finalParent(else_if) = finalParent(the if it continues); the owning
|
|
362
|
+
// if is exactly the containment parent (nothing sits between an
|
|
363
|
+
// if_statement and the if_statement in its else-clause).
|
|
364
|
+
parent = attachedTo.get(containmentParent) ?? root;
|
|
365
|
+
} else {
|
|
366
|
+
parent = containmentParent?.node ?? root;
|
|
367
|
+
}
|
|
368
|
+
parent.children.push(proto.node);
|
|
369
|
+
attachedTo.set(proto, parent);
|
|
370
|
+
containmentStack.push(proto);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---- Condition hoisting (one level; see module doc). ----
|
|
374
|
+
hoistConditionGroups(root);
|
|
375
|
+
|
|
376
|
+
return root;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function decisionOnlyNode(decision: DecisionFact): ComplexityIRNode {
|
|
380
|
+
if (decision.kind === "case") {
|
|
381
|
+
// Case bodies nest one level below the switch head (cognitive.ts's
|
|
382
|
+
// documented boundary list includes "case").
|
|
383
|
+
return {
|
|
384
|
+
kind: "case",
|
|
385
|
+
range: decision.range,
|
|
386
|
+
children: [],
|
|
387
|
+
nestingBoundary: true,
|
|
388
|
+
decision,
|
|
389
|
+
symbolScopeId: decision.symbolScopeId,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (LOGICAL_DECISION_KINDS.has(decision.kind)) {
|
|
393
|
+
return {
|
|
394
|
+
kind: "logical_sequence",
|
|
395
|
+
range: decision.range,
|
|
396
|
+
children: [],
|
|
397
|
+
decision,
|
|
398
|
+
symbolScopeId: decision.symbolScopeId,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
// guard/if/loop/... decisions normally merge with their nesting event by
|
|
402
|
+
// range; a decision kind without one (e.g. "pattern_alternative") becomes
|
|
403
|
+
// a plain non-boundary decision node.
|
|
404
|
+
return {
|
|
405
|
+
kind: "decision",
|
|
406
|
+
range: decision.range,
|
|
407
|
+
children: [],
|
|
408
|
+
decision,
|
|
409
|
+
symbolScopeId: decision.symbolScopeId,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MaintainabilityCalculator (spec.md §16.9). Exact default formula, no
|
|
3
|
+
* comment bonus (v1 deliberately omits it per spec.md §16.9, for
|
|
4
|
+
* cross-language stability), clamped to [0, 100]. The formula string itself
|
|
5
|
+
* is always returned, as required by the work order.
|
|
6
|
+
*/
|
|
7
|
+
import type { MaintainabilityResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const MAINTAINABILITY_FORMULA =
|
|
10
|
+
"MI = clamp((171 - 5.2*ln(max(HalsteadVolume,1)) - 0.23*CyclomaticClassic - 16.2*ln(max(SLOC,1))) * 100 / 171, 0, 100)";
|
|
11
|
+
|
|
12
|
+
export interface MaintainabilityInput {
|
|
13
|
+
halsteadVolume: number;
|
|
14
|
+
cyclomaticClassic: number;
|
|
15
|
+
sloc: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function computeMaintainability(
|
|
19
|
+
input: MaintainabilityInput,
|
|
20
|
+
algorithmVersion = "untangle-maintainability-v1",
|
|
21
|
+
): MaintainabilityResult {
|
|
22
|
+
const volumeTerm = Math.log(Math.max(input.halsteadVolume, 1));
|
|
23
|
+
const slocTerm = Math.log(Math.max(input.sloc, 1));
|
|
24
|
+
const raw = 171 - 5.2 * volumeTerm - 0.23 * input.cyclomaticClassic - 16.2 * slocTerm;
|
|
25
|
+
const scaled = (raw * 100) / 171;
|
|
26
|
+
const value = Math.min(100, Math.max(0, scaled));
|
|
27
|
+
|
|
28
|
+
return { value, raw, formula: MAINTAINABILITY_FORMULA, algorithmVersion };
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort classification of raw, adapter-specific AST node kind strings
|
|
3
|
+
* (`NodeKindFact.kind`, `ComplexityFacts.nodeKinds`) into the language-neutral
|
|
4
|
+
* buckets `StructuralCalculator`/`SyntacticCalculator` need for
|
|
5
|
+
* `parameter_count`, `return_count`, `exit_point_count`, `statement_count`,
|
|
6
|
+
* `expression_count` and `declaration_count` (spec.md §16.5, §16.8).
|
|
7
|
+
*
|
|
8
|
+
* `ComplexityFacts` deliberately keeps `kind` as an opaque, grammar-specific
|
|
9
|
+
* string (e.g. tree-sitter's `"required_parameter"`, `"return_statement"`) —
|
|
10
|
+
* domain does not define a normalized node-kind taxonomy. This module is
|
|
11
|
+
* Untangle's own, documented, versioned answer to that gap (see README
|
|
12
|
+
* "Entscheidungen" #5); it is intentionally conservative (substring match on
|
|
13
|
+
* common, cross-grammar naming conventions) and is flagged in `openIssues` as
|
|
14
|
+
* a candidate for an additive domain extension once real adapter output
|
|
15
|
+
* exists to validate against.
|
|
16
|
+
*/
|
|
17
|
+
export type NodeKindClass =
|
|
18
|
+
"parameter" | "return" | "exit" | "statement" | "expression" | "declaration" | "other";
|
|
19
|
+
|
|
20
|
+
const RETURN_MARKERS = ["return_statement", "return"];
|
|
21
|
+
const EXIT_MARKERS = ["throw_statement", "throw", "break_statement", "continue_statement"];
|
|
22
|
+
const PARAMETER_MARKERS = ["parameter"];
|
|
23
|
+
const DECLARATION_MARKERS = ["declaration", "declarator"];
|
|
24
|
+
const STATEMENT_MARKERS = ["statement"];
|
|
25
|
+
const EXPRESSION_MARKERS = ["expression"];
|
|
26
|
+
|
|
27
|
+
function matches(normalizedKind: string, markers: readonly string[]): boolean {
|
|
28
|
+
return markers.some((marker) => normalizedKind === marker || normalizedKind.includes(marker));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Classifies one raw node-kind string. Order matters: more specific buckets are checked first. */
|
|
32
|
+
export function classifyNodeKind(kind: string): NodeKindClass {
|
|
33
|
+
const normalized = kind.toLowerCase();
|
|
34
|
+
if (matches(normalized, RETURN_MARKERS)) return "return";
|
|
35
|
+
if (matches(normalized, EXIT_MARKERS)) return "exit";
|
|
36
|
+
if (matches(normalized, PARAMETER_MARKERS)) return "parameter";
|
|
37
|
+
if (matches(normalized, DECLARATION_MARKERS)) return "declaration";
|
|
38
|
+
if (matches(normalized, STATEMENT_MARKERS)) return "statement";
|
|
39
|
+
if (matches(normalized, EXPRESSION_MARKERS)) return "expression";
|
|
40
|
+
return "other";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ASYNC_MARKERS = ["async", "await"];
|
|
44
|
+
|
|
45
|
+
/** True when a raw node-kind string looks like an async construct (best-effort, see module docs). */
|
|
46
|
+
export function isAsyncNodeKind(kind: string): boolean {
|
|
47
|
+
return matches(kind.toLowerCase(), ASYNC_MARKERS);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const EXCEPTION_MARKERS = ["throw_statement", "throw", "try_statement", "catch_clause"];
|
|
51
|
+
|
|
52
|
+
/** True when a raw node-kind string looks like an exception construct (best-effort, see module docs). */
|
|
53
|
+
export function isExceptionNodeKind(kind: string): boolean {
|
|
54
|
+
return matches(kind.toLowerCase(), EXCEPTION_MARKERS);
|
|
55
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prosa-Metriken (vision.md §1c/§1d, WP-217).
|
|
3
|
+
*
|
|
4
|
+
* WARUM ES DIESE DATEI GIBT
|
|
5
|
+
*
|
|
6
|
+
* Alles andere in diesem Paket rechnet ueber `ComplexityFacts`: Scopes,
|
|
7
|
+
* Entscheidungspunkte, Verschachtelung, Operatoren. Ein Markdown-Dokument
|
|
8
|
+
* hat davon NICHTS — nicht wenig, sondern nichts. Der Prosa-Adapter liefert
|
|
9
|
+
* deshalb leere Facts, und Prosa wird hier ueber ihre EIGENEN Groessen
|
|
10
|
+
* gemessen: Struktur- und Verweislast.
|
|
11
|
+
*
|
|
12
|
+
* Die Trennung ist strukturell, nicht per Konvention: jeder Rueckgabewert
|
|
13
|
+
* ist ein {@link ProseNumberV1}, also ein Objekt. Ein Prosa-Messwert kann
|
|
14
|
+
* dadurch an keiner Stelle stehen, an der `computeCognitive` oder
|
|
15
|
+
* `aggregate` eine Zahl erwarten — der Uebersetzer lehnt es ab, und
|
|
16
|
+
* `assertUntangleMetricNumber` lehnt es zur Laufzeit ab. Vorbild ist
|
|
17
|
+
* `ExternalNumberV1` (domain `types/extensions.ts`), das externe Befunde
|
|
18
|
+
* genauso von Untangle-Metriken fernhaelt.
|
|
19
|
+
*
|
|
20
|
+
* DIE FUENF MASSE, UND WARUM GENAU DIESE
|
|
21
|
+
*
|
|
22
|
+
* Jedes Mass muss ohne Modellannahme lesbar sein — sonst ist es eine
|
|
23
|
+
* erfundene Zahl mit Nachkommastellen. Was hier steht, laesst sich am
|
|
24
|
+
* Dokument nachzaehlen:
|
|
25
|
+
*
|
|
26
|
+
* 1. `sectionDepth` — tiefste benutzte Ueberschriftenebene.
|
|
27
|
+
* 2. `maxSectionLines` — laengster Abschnitt in Zeilen.
|
|
28
|
+
* 3. `linkDensity` — Verweise je 100 Inhaltszeilen.
|
|
29
|
+
* 4. `unresolvedLinkRatio` — Anteil toter relativer Dateiverweise, oder
|
|
30
|
+
* `null`, wenn niemand aufgeloest hat.
|
|
31
|
+
* 5. `repetitionRatio` — Anteil woertlich wiederholter Inhaltszeilen.
|
|
32
|
+
*
|
|
33
|
+
* Dazu der abgeleitete Ordnungsschluessel `structureLoad`: die Summe der
|
|
34
|
+
* Ueberschriftenebenen aller Abschnitte. Bewusst keine gewichtete Mischung
|
|
35
|
+
* ungleicher Groessen — das waere der eine „Quality Score", den vision.md
|
|
36
|
+
* §7.6 ablehnt —, sondern eine abzaehlbare Groesse: die Gesamtzahl der
|
|
37
|
+
* Gliederungsstufen, die das Dokument aufspannt. Ein Dokument mit 40
|
|
38
|
+
* Abschnitten auf Ebene 3 traegt mehr Gliederung als eines mit 40
|
|
39
|
+
* Abschnitten auf Ebene 1, und beide mehr als eines mit vier.
|
|
40
|
+
*
|
|
41
|
+
* DER AUFLOESER
|
|
42
|
+
*
|
|
43
|
+
* `unresolvedLinkRatio` ist der einzige Wert, der etwas ausserhalb des
|
|
44
|
+
* Dokuments braucht. Ohne `resolveLink` bleibt er `null` — ein
|
|
45
|
+
* Zustandsvermerk, kein Messwert von 0. vision.md §1d ist an genau dieser
|
|
46
|
+
* Stelle deutlich: „niemand hat gesucht" darf nie wie ein Befund ueber den
|
|
47
|
+
* Inhalt aussehen. Deshalb ist der Aufloeser ein Parameter und keine
|
|
48
|
+
* Dateizugriffsfunktion in diesem Paket: `@untangle-code/complexity` bleibt
|
|
49
|
+
* frei von I/O.
|
|
50
|
+
*/
|
|
51
|
+
import type {
|
|
52
|
+
ProseLinkV1,
|
|
53
|
+
ProseMetricsV1,
|
|
54
|
+
ProseNumberV1,
|
|
55
|
+
ProseSectionMetricV1,
|
|
56
|
+
} from "@untangle-code/domain";
|
|
57
|
+
import {
|
|
58
|
+
PROSE_METRICS_VERSION,
|
|
59
|
+
classifyLinkTarget,
|
|
60
|
+
countContentLines,
|
|
61
|
+
findMarkdownLinks,
|
|
62
|
+
linkFilePath,
|
|
63
|
+
makeProseNumber,
|
|
64
|
+
normalizedContentLines,
|
|
65
|
+
outlineMarkdown,
|
|
66
|
+
} from "@untangle-code/domain";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Beantwortet, ob das Ziel eines relativen Dateiverweises existiert.
|
|
70
|
+
* `documentPath` ist der Pfad des VERWEISENDEN Dokuments, damit der
|
|
71
|
+
* Aufloeser relativ dazu suchen kann.
|
|
72
|
+
*/
|
|
73
|
+
export type ProseLinkResolver = (documentPath: string, targetFilePath: string) => boolean;
|
|
74
|
+
|
|
75
|
+
export interface ProseMetricsOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Ohne Aufloeser bleibt `unresolvedLinkRatio` `null`. Das ist Absicht:
|
|
78
|
+
* ein ungeprueftes 0 waere die Behauptung, alle Verweise seien heil.
|
|
79
|
+
*/
|
|
80
|
+
resolveLink?: ProseLinkResolver;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Physische Zeilen ohne Zeilenende; ein abschliessendes `\r` faellt weg. */
|
|
84
|
+
function splitLines(content: string): string[] {
|
|
85
|
+
return content.split("\n").map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Rundet auf eine Nachkommastelle. Verhaeltniswerte werden gespeichert und
|
|
90
|
+
* verglichen; ohne feste Rundung waeren zwei Laeufe ueber dieselbe Datei
|
|
91
|
+
* durch Gleitkommarauschen nicht bitgleich.
|
|
92
|
+
*/
|
|
93
|
+
function round(value: number, digits: number): number {
|
|
94
|
+
const factor = 10 ** digits;
|
|
95
|
+
return Math.round(value * factor) / factor;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Prosa-Metriken eines Dokuments.
|
|
100
|
+
*
|
|
101
|
+
* `path` ist der "/"-normalisierte, repo-relative Pfad; er wird nur
|
|
102
|
+
* durchgereicht (an den Aufloeser und in das Ergebnis), nie geoeffnet.
|
|
103
|
+
*/
|
|
104
|
+
export function computeProseMetrics(
|
|
105
|
+
path: string,
|
|
106
|
+
content: string,
|
|
107
|
+
options: ProseMetricsOptions = {},
|
|
108
|
+
): ProseMetricsV1 {
|
|
109
|
+
const lines = splitLines(content);
|
|
110
|
+
const outline = outlineMarkdown(lines);
|
|
111
|
+
|
|
112
|
+
const sections: ProseSectionMetricV1[] = outline.map((section) => ({
|
|
113
|
+
breadcrumb: section.breadcrumb,
|
|
114
|
+
level: section.level,
|
|
115
|
+
startLine: section.startLine,
|
|
116
|
+
endLine: section.endLine,
|
|
117
|
+
lines: Math.max(0, section.endLine - section.startLine + 1),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const contentLines = countContentLines(lines);
|
|
121
|
+
const rawLinks = findMarkdownLinks(lines);
|
|
122
|
+
|
|
123
|
+
const resolver = options.resolveLink;
|
|
124
|
+
const links: ProseLinkV1[] = rawLinks.map((link) => {
|
|
125
|
+
const kind = classifyLinkTarget(link.target);
|
|
126
|
+
let resolved: boolean | null = null;
|
|
127
|
+
if (kind === "relative_file" && resolver !== undefined) {
|
|
128
|
+
const target = linkFilePath(link.target);
|
|
129
|
+
// Ein Verweis, der nur auf einen Anker im selben Dokument zeigt, hat
|
|
130
|
+
// keinen Dateiteil; er ist nicht "tot", er ist keine Dateikante.
|
|
131
|
+
resolved = target === "" ? true : resolver(path, target);
|
|
132
|
+
}
|
|
133
|
+
return { target: link.target, text: link.text, line: link.line, kind, resolved };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const resolvable = links.filter((link) => link.resolved !== null);
|
|
137
|
+
const unresolved = resolvable.filter((link) => link.resolved === false);
|
|
138
|
+
|
|
139
|
+
const normalized = normalizedContentLines(lines);
|
|
140
|
+
const seen = new Set<string>();
|
|
141
|
+
let repeated = 0;
|
|
142
|
+
for (const line of normalized) {
|
|
143
|
+
if (seen.has(line)) repeated += 1;
|
|
144
|
+
else seen.add(line);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const depth = sections.reduce((max, section) => Math.max(max, section.level), 0);
|
|
148
|
+
const maxLines = sections.reduce((max, section) => Math.max(max, section.lines), 0);
|
|
149
|
+
const structureLoad = sections.reduce((sum, section) => sum + section.level, 0);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
metricsVersion: PROSE_METRICS_VERSION,
|
|
154
|
+
path,
|
|
155
|
+
sectionDepth: makeProseNumber(depth, "levels"),
|
|
156
|
+
maxSectionLines: makeProseNumber(maxLines, "lines"),
|
|
157
|
+
linkDensity: makeProseNumber(
|
|
158
|
+
contentLines === 0 ? 0 : round((links.length / contentLines) * 100, 1),
|
|
159
|
+
"per_100_lines",
|
|
160
|
+
),
|
|
161
|
+
unresolvedLinkRatio:
|
|
162
|
+
resolvable.length === 0
|
|
163
|
+
? null
|
|
164
|
+
: makeProseNumber(round(unresolved.length / resolvable.length, 4), "ratio"),
|
|
165
|
+
repetitionRatio: makeProseNumber(
|
|
166
|
+
normalized.length === 0 ? 0 : round(repeated / normalized.length, 4),
|
|
167
|
+
"ratio",
|
|
168
|
+
),
|
|
169
|
+
structureLoad: makeProseNumber(structureLoad, "count"),
|
|
170
|
+
sectionCount: makeProseNumber(sections.length, "count"),
|
|
171
|
+
contentLines: makeProseNumber(contentLines, "count"),
|
|
172
|
+
sections,
|
|
173
|
+
links,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Ordnet zwei Dokumente nach Strukturlast, absteigend, mit vollstaendig
|
|
179
|
+
* deterministischem Gleichstandsbruch (nie `localeCompare`).
|
|
180
|
+
*
|
|
181
|
+
* `compare` liefert eine Ordnung, keine Bewertung: das Dokument mit der
|
|
182
|
+
* hoechsten Strukturlast ist nicht „das schlechteste", sondern das mit der
|
|
183
|
+
* meisten Gliederung — die Deutung gehoert dem Leser, die Zahl dem Werkzeug.
|
|
184
|
+
*/
|
|
185
|
+
export function compareProseByStructureLoad(a: ProseMetricsV1, b: ProseMetricsV1): number {
|
|
186
|
+
const byLoad = valueOf(b.structureLoad) - valueOf(a.structureLoad);
|
|
187
|
+
if (byLoad !== 0) return byLoad;
|
|
188
|
+
const byDepth = valueOf(b.sectionDepth) - valueOf(a.sectionDepth);
|
|
189
|
+
if (byDepth !== 0) return byDepth;
|
|
190
|
+
return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Der Rohwert eines Prosa-Messwerts fuer die SORTIERUNG innerhalb dieser
|
|
195
|
+
* Familie. Bewusst paketlokal und nicht exportiert: eine allgemein
|
|
196
|
+
* verfuegbare `number`-Entpackung waere genau das Loch, das
|
|
197
|
+
* {@link ProseNumberV1} zumauert.
|
|
198
|
+
*/
|
|
199
|
+
function valueOf(value: ProseNumberV1): number {
|
|
200
|
+
return value.value;
|
|
201
|
+
}
|