@hydranium/cli 1.0.0-next.10
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 +21 -0
- package/README.md +240 -0
- package/heap-analysis/analyze-heap.mjs +798 -0
- package/heap-analysis/heap/core/aggregate.mjs +152 -0
- package/heap-analysis/heap/core/bucket-label.mjs +23 -0
- package/heap-analysis/heap/core/cutpoints.mjs +51 -0
- package/heap-analysis/heap/core/diff.mjs +56 -0
- package/heap-analysis/heap/core/format.mjs +47 -0
- package/heap-analysis/heap/core/holders.mjs +80 -0
- package/heap-analysis/heap/core/load.mjs +46 -0
- package/heap-analysis/heap/core/report.mjs +68 -0
- package/heap-analysis/heap/core/strings.mjs +80 -0
- package/heap-analysis/heap/langium/concepts.mjs +230 -0
- package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
- package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
- package/heap-analysis/heap/renderer/renderer.mjs +122 -0
- package/lib/cli-version.d.ts +21 -0
- package/lib/cli-version.d.ts.map +1 -0
- package/lib/cli-version.js +30 -0
- package/lib/cli-version.js.map +1 -0
- package/lib/cli.d.ts +11 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +135 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/analyze-heap-args.d.ts +46 -0
- package/lib/commands/analyze-heap-args.d.ts.map +1 -0
- package/lib/commands/analyze-heap-args.js +174 -0
- package/lib/commands/analyze-heap-args.js.map +1 -0
- package/lib/commands/analyze-heap.d.ts +37 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -0
- package/lib/commands/analyze-heap.js +63 -0
- package/lib/commands/analyze-heap.js.map +1 -0
- package/lib/commands/ast-ground-truth-args.d.ts +23 -0
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-args.js +47 -0
- package/lib/commands/ast-ground-truth-args.js.map +1 -0
- package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
- package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-driver.js +72 -0
- package/lib/commands/ast-ground-truth-driver.js.map +1 -0
- package/lib/commands/ast-ground-truth.d.ts +31 -0
- package/lib/commands/ast-ground-truth.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth.js +36 -0
- package/lib/commands/ast-ground-truth.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts +38 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-args.js +183 -0
- package/lib/commands/generate-transfer-model-args.js.map +1 -0
- package/lib/commands/generate-transfer-model-config.d.ts +32 -0
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-config.js +122 -0
- package/lib/commands/generate-transfer-model-config.js.map +1 -0
- package/lib/commands/generate-transfer-model.d.ts +110 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model.js +587 -0
- package/lib/commands/generate-transfer-model.js.map +1 -0
- package/lib/commands/harness-args.d.ts +198 -0
- package/lib/commands/harness-args.d.ts.map +1 -0
- package/lib/commands/harness-args.js +339 -0
- package/lib/commands/harness-args.js.map +1 -0
- package/lib/commands/headless-harness.d.ts +97 -0
- package/lib/commands/headless-harness.d.ts.map +1 -0
- package/lib/commands/headless-harness.js +121 -0
- package/lib/commands/headless-harness.js.map +1 -0
- package/lib/commands/init-args.d.ts +81 -0
- package/lib/commands/init-args.d.ts.map +1 -0
- package/lib/commands/init-args.js +250 -0
- package/lib/commands/init-args.js.map +1 -0
- package/lib/commands/init-prompt.d.ts +66 -0
- package/lib/commands/init-prompt.d.ts.map +1 -0
- package/lib/commands/init-prompt.js +71 -0
- package/lib/commands/init-prompt.js.map +1 -0
- package/lib/commands/init-templates.d.ts +42 -0
- package/lib/commands/init-templates.d.ts.map +1 -0
- package/lib/commands/init-templates.js +1902 -0
- package/lib/commands/init-templates.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +65 -0
- package/lib/commands/init-wizard.d.ts.map +1 -0
- package/lib/commands/init-wizard.js +302 -0
- package/lib/commands/init-wizard.js.map +1 -0
- package/lib/commands/init-workspace.d.ts +168 -0
- package/lib/commands/init-workspace.d.ts.map +1 -0
- package/lib/commands/init-workspace.js +372 -0
- package/lib/commands/init-workspace.js.map +1 -0
- package/lib/commands/init.d.ts +273 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +303 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +23 -0
- package/lib/commands/lint-grammar-args.d.ts.map +1 -0
- package/lib/commands/lint-grammar-args.js +63 -0
- package/lib/commands/lint-grammar-args.js.map +1 -0
- package/lib/commands/lint-grammar-driver.d.ts +10 -0
- package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
- package/lib/commands/lint-grammar-driver.js +72 -0
- package/lib/commands/lint-grammar-driver.js.map +1 -0
- package/lib/commands/lint-grammar-report.d.ts +24 -0
- package/lib/commands/lint-grammar-report.d.ts.map +1 -0
- package/lib/commands/lint-grammar-report.js +65 -0
- package/lib/commands/lint-grammar-report.js.map +1 -0
- package/lib/commands/lint-grammar.d.ts +33 -0
- package/lib/commands/lint-grammar.d.ts.map +1 -0
- package/lib/commands/lint-grammar.js +42 -0
- package/lib/commands/lint-grammar.js.map +1 -0
- package/lib/commands/measure-memory-args.d.ts +23 -0
- package/lib/commands/measure-memory-args.d.ts.map +1 -0
- package/lib/commands/measure-memory-args.js +106 -0
- package/lib/commands/measure-memory-args.js.map +1 -0
- package/lib/commands/measure-memory-driver.d.ts +10 -0
- package/lib/commands/measure-memory-driver.d.ts.map +1 -0
- package/lib/commands/measure-memory-driver.js +110 -0
- package/lib/commands/measure-memory-driver.js.map +1 -0
- package/lib/commands/measure-memory.d.ts +68 -0
- package/lib/commands/measure-memory.d.ts.map +1 -0
- package/lib/commands/measure-memory.js +100 -0
- package/lib/commands/measure-memory.js.map +1 -0
- package/lib/commands/model-docs-args.d.ts +23 -0
- package/lib/commands/model-docs-args.d.ts.map +1 -0
- package/lib/commands/model-docs-args.js +53 -0
- package/lib/commands/model-docs-args.js.map +1 -0
- package/lib/commands/model-docs-driver.d.ts +10 -0
- package/lib/commands/model-docs-driver.d.ts.map +1 -0
- package/lib/commands/model-docs-driver.js +61 -0
- package/lib/commands/model-docs-driver.js.map +1 -0
- package/lib/commands/model-docs-report.d.ts +18 -0
- package/lib/commands/model-docs-report.d.ts.map +1 -0
- package/lib/commands/model-docs-report.js +123 -0
- package/lib/commands/model-docs-report.js.map +1 -0
- package/lib/commands/model-docs.d.ts +30 -0
- package/lib/commands/model-docs.d.ts.map +1 -0
- package/lib/commands/model-docs.js +37 -0
- package/lib/commands/model-docs.js.map +1 -0
- package/lib/commands/projects-args.d.ts +25 -0
- package/lib/commands/projects-args.d.ts.map +1 -0
- package/lib/commands/projects-args.js +49 -0
- package/lib/commands/projects-args.js.map +1 -0
- package/lib/commands/projects.d.ts +45 -0
- package/lib/commands/projects.d.ts.map +1 -0
- package/lib/commands/projects.js +39 -0
- package/lib/commands/projects.js.map +1 -0
- package/lib/commands/query-args.d.ts +25 -0
- package/lib/commands/query-args.d.ts.map +1 -0
- package/lib/commands/query-args.js +46 -0
- package/lib/commands/query-args.js.map +1 -0
- package/lib/commands/query.d.ts +32 -0
- package/lib/commands/query.d.ts.map +1 -0
- package/lib/commands/query.js +29 -0
- package/lib/commands/query.js.map +1 -0
- package/lib/commands/reflect-args.d.ts +23 -0
- package/lib/commands/reflect-args.d.ts.map +1 -0
- package/lib/commands/reflect-args.js +59 -0
- package/lib/commands/reflect-args.js.map +1 -0
- package/lib/commands/reflect-driver.d.ts +10 -0
- package/lib/commands/reflect-driver.d.ts.map +1 -0
- package/lib/commands/reflect-driver.js +66 -0
- package/lib/commands/reflect-driver.js.map +1 -0
- package/lib/commands/reflect-report.d.ts +20 -0
- package/lib/commands/reflect-report.d.ts.map +1 -0
- package/lib/commands/reflect-report.js +127 -0
- package/lib/commands/reflect-report.js.map +1 -0
- package/lib/commands/reflect.d.ts +30 -0
- package/lib/commands/reflect.d.ts.map +1 -0
- package/lib/commands/reflect.js +38 -0
- package/lib/commands/reflect.js.map +1 -0
- package/lib/commands/save-args.d.ts +26 -0
- package/lib/commands/save-args.d.ts.map +1 -0
- package/lib/commands/save-args.js +59 -0
- package/lib/commands/save-args.js.map +1 -0
- package/lib/commands/save.d.ts +38 -0
- package/lib/commands/save.d.ts.map +1 -0
- package/lib/commands/save.js +48 -0
- package/lib/commands/save.js.map +1 -0
- package/lib/commands/validate-args.d.ts +23 -0
- package/lib/commands/validate-args.d.ts.map +1 -0
- package/lib/commands/validate-args.js +60 -0
- package/lib/commands/validate-args.js.map +1 -0
- package/lib/commands/validate-driver.d.ts +10 -0
- package/lib/commands/validate-driver.d.ts.map +1 -0
- package/lib/commands/validate-driver.js +72 -0
- package/lib/commands/validate-driver.js.map +1 -0
- package/lib/commands/validate-report.d.ts +30 -0
- package/lib/commands/validate-report.d.ts.map +1 -0
- package/lib/commands/validate-report.js +87 -0
- package/lib/commands/validate-report.js.map +1 -0
- package/lib/commands/validate.d.ts +34 -0
- package/lib/commands/validate.d.ts.map +1 -0
- package/lib/commands/validate.js +41 -0
- package/lib/commands/validate.js.map +1 -0
- package/lib/commands/watch-args.d.ts +26 -0
- package/lib/commands/watch-args.d.ts.map +1 -0
- package/lib/commands/watch-args.js +59 -0
- package/lib/commands/watch-args.js.map +1 -0
- package/lib/commands/watch.d.ts +75 -0
- package/lib/commands/watch.d.ts.map +1 -0
- package/lib/commands/watch.js +112 -0
- package/lib/commands/watch.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/log-level.d.ts +39 -0
- package/lib/log-level.d.ts.map +1 -0
- package/lib/log-level.js +47 -0
- package/lib/log-level.js.map +1 -0
- package/lib/package-name.d.ts +33 -0
- package/lib/package-name.d.ts.map +1 -0
- package/lib/package-name.js +37 -0
- package/lib/package-name.js.map +1 -0
- package/lib/spawn-data-server.d.ts +102 -0
- package/lib/spawn-data-server.d.ts.map +1 -0
- package/lib/spawn-data-server.js +154 -0
- package/lib/spawn-data-server.js.map +1 -0
- package/lib/testing/echo-server.d.ts +11 -0
- package/lib/testing/echo-server.d.ts.map +1 -0
- package/lib/testing/echo-server.js +79 -0
- package/lib/testing/echo-server.js.map +1 -0
- package/package.json +105 -0
- package/src/cli-version.ts +32 -0
- package/src/cli.ts +139 -0
- package/src/commands/analyze-heap-args.ts +184 -0
- package/src/commands/analyze-heap.ts +76 -0
- package/src/commands/ast-ground-truth-args.ts +63 -0
- package/src/commands/ast-ground-truth-driver.ts +81 -0
- package/src/commands/ast-ground-truth.ts +49 -0
- package/src/commands/generate-transfer-model-args.ts +203 -0
- package/src/commands/generate-transfer-model-config.ts +128 -0
- package/src/commands/generate-transfer-model.ts +716 -0
- package/src/commands/harness-args.ts +429 -0
- package/src/commands/headless-harness.ts +165 -0
- package/src/commands/init-args.ts +273 -0
- package/src/commands/init-prompt.ts +149 -0
- package/src/commands/init-templates.ts +2037 -0
- package/src/commands/init-wizard.ts +378 -0
- package/src/commands/init-workspace.ts +442 -0
- package/src/commands/init.ts +554 -0
- package/src/commands/lint-grammar-args.ts +79 -0
- package/src/commands/lint-grammar-driver.ts +83 -0
- package/src/commands/lint-grammar-report.ts +74 -0
- package/src/commands/lint-grammar.ts +57 -0
- package/src/commands/measure-memory-args.ts +123 -0
- package/src/commands/measure-memory-driver.ts +128 -0
- package/src/commands/measure-memory.ts +143 -0
- package/src/commands/model-docs-args.ts +69 -0
- package/src/commands/model-docs-driver.ts +68 -0
- package/src/commands/model-docs-report.ts +133 -0
- package/src/commands/model-docs.ts +48 -0
- package/src/commands/projects-args.ts +62 -0
- package/src/commands/projects.ts +76 -0
- package/src/commands/query-args.ts +61 -0
- package/src/commands/query.ts +58 -0
- package/src/commands/reflect-args.ts +75 -0
- package/src/commands/reflect-driver.ts +76 -0
- package/src/commands/reflect-report.ts +138 -0
- package/src/commands/reflect.ts +51 -0
- package/src/commands/save-args.ts +74 -0
- package/src/commands/save.ts +84 -0
- package/src/commands/validate-args.ts +76 -0
- package/src/commands/validate-driver.ts +84 -0
- package/src/commands/validate-report.ts +95 -0
- package/src/commands/validate.ts +58 -0
- package/src/commands/watch-args.ts +74 -0
- package/src/commands/watch.ts +185 -0
- package/src/index.ts +16 -0
- package/src/log-level.ts +49 -0
- package/src/package-name.ts +39 -0
- package/src/spawn-data-server.ts +247 -0
- package/src/testing/echo-server.ts +93 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Concept-classification engine: shared heap-node helpers, the primitive / plain
|
|
12
|
+
* fallback (V8/JS universals, no language vocabulary), and `composeClassifiers`,
|
|
13
|
+
* which folds a set of ConceptClassifier contributions into the `classify` /
|
|
14
|
+
* `isAnchorConcept` / `conceptGroup` / `grammarTypes` the analyzer consumes.
|
|
15
|
+
*
|
|
16
|
+
* The engine knows no model vocabulary of its own — Langium and GLSP knowledge
|
|
17
|
+
* live in classifier contributions loaded by default, exactly the contract an
|
|
18
|
+
* adopter's `--classifier` file uses.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ConceptClassifier
|
|
23
|
+
* @property {(node: import('@memlab/core').IHeapNode) => (string|undefined)} classify
|
|
24
|
+
* Map a heap node to a concept label, or `undefined` to defer to the next
|
|
25
|
+
* classifier / the engine fallback. Receives only `object`-typed nodes
|
|
26
|
+
* (primitives are the engine's baseline).
|
|
27
|
+
* @property {string[]} [anchorConcepts] Concept labels this classifier owns as
|
|
28
|
+
* exclusive-retained attribution anchors.
|
|
29
|
+
* @property {string[]} [grammarTypes] `$type`s from a meta-grammar that appear in
|
|
30
|
+
* the heap but not an adopter document model (e.g. Langium's own grammar AST).
|
|
31
|
+
* @property {(label: string) => boolean} [isAnchor] Predicate for prefix-based
|
|
32
|
+
* anchors (e.g. `AST node:*`); supplements `anchorConcepts`.
|
|
33
|
+
* @property {Record<string, string>} [descriptions] One-line explanations keyed by
|
|
34
|
+
* root group (first label segment, e.g. `GModel`, `CST`), shown under that
|
|
35
|
+
* group's heading in the hierarchical breakdown.
|
|
36
|
+
*
|
|
37
|
+
* A label is a `:`-delimited HIERARCHY PATH (`L1:L2:L3`, see {@link parseLabelPath}):
|
|
38
|
+
* the first segment is the display group and the analyzer aggregates at every
|
|
39
|
+
* prefix, so a classifier never declares a group — it just mints a path. Each
|
|
40
|
+
* segment must be `:`-free (spaces are fine); ≤3 levels is the readability
|
|
41
|
+
* convention, but aggregation is depth-agnostic and never truncates.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** Property-name set of a node's own named properties. */
|
|
45
|
+
export function propertySet(node) {
|
|
46
|
+
const set = new Set();
|
|
47
|
+
for (const edge of node.references) {
|
|
48
|
+
if (edge.type === 'property' && !edge.is_index && edge.name_or_index !== '__proto__') {
|
|
49
|
+
set.add(edge.name_or_index);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return set;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Value of a string-valued property edge (e.g. the `$type` element kind). */
|
|
56
|
+
export function stringProp(node, name) {
|
|
57
|
+
for (const edge of node.references) {
|
|
58
|
+
if (edge.type === 'property' && edge.name_or_index === name) {
|
|
59
|
+
const target = edge.toNode;
|
|
60
|
+
if (target && (target.type === 'string' || target.type === 'concatenated string')) {
|
|
61
|
+
return target.name;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True when a property set contains every named property. */
|
|
70
|
+
export const has = (set, ...names) => names.every(name => set.has(name));
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Follow a property edge to its target object and read a string property off it
|
|
74
|
+
* (one hop of dereference). Used where a discriminator lives on a referenced node
|
|
75
|
+
* rather than inline — e.g. a leaf CST node's token type is `_tokenType.name`.
|
|
76
|
+
*/
|
|
77
|
+
export function derefStringProp(node, edgeName, prop) {
|
|
78
|
+
for (const edge of node.references) {
|
|
79
|
+
if (edge.type === 'property' && edge.name_or_index === edgeName && edge.toNode) {
|
|
80
|
+
return stringProp(edge.toNode, prop);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read a discriminator property (e.g. `$type`/`type`) as a usable label segment.
|
|
88
|
+
* A concatenated/sliced string value cannot be materialized by memlab — it
|
|
89
|
+
* surfaces as a placeholder like `(concatenated string)`; treat any such
|
|
90
|
+
* unreadable value (and a missing one) as `unknown` rather than leaking the
|
|
91
|
+
* placeholder into a concept label.
|
|
92
|
+
*/
|
|
93
|
+
export function readableType(node, name = 'type') {
|
|
94
|
+
const value = stringProp(node, name);
|
|
95
|
+
return value && !value.startsWith('(') ? value : 'unknown';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Split a `:`-delimited label into its hierarchy segments: the first segment is
|
|
100
|
+
* the display group and each deeper segment a drill-down level. ≤3 levels is the
|
|
101
|
+
* convention, but parsing is depth-agnostic — it never truncates, so an adopter
|
|
102
|
+
* that emits a deeper path simply gets a deeper tree (and a `:`-free label is a
|
|
103
|
+
* 1-level leaf, which is always valid). Empty/blank → a single `(unknown)`
|
|
104
|
+
* segment so callers always get a non-empty group.
|
|
105
|
+
*/
|
|
106
|
+
export function parseLabelPath(label) {
|
|
107
|
+
const trimmed = (label ?? '').trim();
|
|
108
|
+
return trimmed ? trimmed.split(':') : ['(unknown)'];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Aggregate labelled buckets into a hierarchy keyed by the `:`-delimited label
|
|
113
|
+
* path. EVERY prefix accumulates, so a label `A:B:C` adds its sizes to nodes `A`,
|
|
114
|
+
* `A→B` and `A→B→C`; a parent's sizes are therefore exactly the sum of its
|
|
115
|
+
* descendants' leaf buckets (the reconciliation invariant the report relies on).
|
|
116
|
+
* Each tree node is `{ sizes: {count, shallow, exclusive, overlapping}, children:
|
|
117
|
+
* Map<segment, node> }`. No concept is hardcoded — the tree shape is whatever the
|
|
118
|
+
* labels imply.
|
|
119
|
+
*
|
|
120
|
+
* @param {Iterable<[string, {count: number, shallow: number, retainedOverlapping: number}]>} buckets
|
|
121
|
+
* `[label, byBucket-entry]` pairs (the shallow/overlapping/count view per label).
|
|
122
|
+
* @param {(label: string) => number} exclusiveOf exclusive-retained bytes for a label.
|
|
123
|
+
* @returns {Map<string, {sizes: object, children: Map}>} the L1 group → subtree map.
|
|
124
|
+
*/
|
|
125
|
+
export function buildHierarchy(buckets, exclusiveOf) {
|
|
126
|
+
const root = new Map();
|
|
127
|
+
const ensure = (map, segment) => {
|
|
128
|
+
let node = map.get(segment);
|
|
129
|
+
if (!node) {
|
|
130
|
+
node = { sizes: { count: 0, shallow: 0, exclusive: 0, overlapping: 0 }, children: new Map() };
|
|
131
|
+
map.set(segment, node);
|
|
132
|
+
}
|
|
133
|
+
return node;
|
|
134
|
+
};
|
|
135
|
+
for (const [label, entry] of buckets) {
|
|
136
|
+
const exclusive = exclusiveOf(label);
|
|
137
|
+
let level = root;
|
|
138
|
+
for (const segment of parseLabelPath(label)) {
|
|
139
|
+
const node = ensure(level, segment);
|
|
140
|
+
node.sizes.count += entry.count;
|
|
141
|
+
node.sizes.shallow += entry.shallow;
|
|
142
|
+
node.sizes.exclusive += exclusive;
|
|
143
|
+
node.sizes.overlapping += entry.retainedOverlapping;
|
|
144
|
+
level = node.children;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return root;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Engine baseline for non-`object` heap nodes — V8/JS universals, no vocabulary. */
|
|
151
|
+
function classifyPrimitive(type) {
|
|
152
|
+
switch (type) {
|
|
153
|
+
case 'string':
|
|
154
|
+
return 'string';
|
|
155
|
+
case 'concatenated string':
|
|
156
|
+
return 'string (concatenated)';
|
|
157
|
+
case 'sliced string':
|
|
158
|
+
return 'string (sliced)';
|
|
159
|
+
case 'number':
|
|
160
|
+
return 'boxed number';
|
|
161
|
+
case 'closure':
|
|
162
|
+
return 'closure';
|
|
163
|
+
case 'code':
|
|
164
|
+
case 'compiled code':
|
|
165
|
+
return 'code';
|
|
166
|
+
case 'array':
|
|
167
|
+
return 'array (backing)';
|
|
168
|
+
case 'hidden':
|
|
169
|
+
return 'hidden';
|
|
170
|
+
case 'synthetic':
|
|
171
|
+
return 'synthetic (roots)';
|
|
172
|
+
case 'native':
|
|
173
|
+
return 'native';
|
|
174
|
+
default:
|
|
175
|
+
return `(${type})`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Engine fallback for an `object` node no classifier claimed. */
|
|
180
|
+
function classifyObjectFallback(node) {
|
|
181
|
+
if (node.name && node.name !== 'Object') {
|
|
182
|
+
return `class:${node.name}`;
|
|
183
|
+
}
|
|
184
|
+
return propertySet(node).size === 0 ? 'empty object {}' : 'other Object shape';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Fold a set of {@link ConceptClassifier} contributions into the classification
|
|
189
|
+
* functions the analyzer consumes. Contributions are tried in order — first
|
|
190
|
+
* non-`undefined` `classify` wins — with the engine's primitive handling before
|
|
191
|
+
* them and its `class:<name>` / `other Object shape` fallback after. Anchors are
|
|
192
|
+
* the union of every contribution's `anchorConcepts` plus any `isAnchor`
|
|
193
|
+
* predicate; `grammarTypes` is the union of all contributions' grammar `$type`s.
|
|
194
|
+
*
|
|
195
|
+
* `conceptGroup` is structural, NOT contributed: a label's group is the first
|
|
196
|
+
* segment of its `:`-delimited path (see {@link parseLabelPath}). Since
|
|
197
|
+
* classifiers mint the label, the first segment IS the display group — there is
|
|
198
|
+
* no alias/rename layer to keep in sync.
|
|
199
|
+
*
|
|
200
|
+
* @param {ConceptClassifier[]} contributions
|
|
201
|
+
*/
|
|
202
|
+
export function composeClassifiers(contributions) {
|
|
203
|
+
const anchorSet = new Set(contributions.flatMap(contribution => contribution.anchorConcepts ?? []));
|
|
204
|
+
const grammarTypes = new Set(contributions.flatMap(contribution => contribution.grammarTypes ?? []));
|
|
205
|
+
// Merge group descriptions; earlier contributions win (adopter overrides default),
|
|
206
|
+
// so apply defaults first and the earlier contributions last.
|
|
207
|
+
const descriptions = Object.assign({}, ...[...contributions].reverse().map(contribution => contribution.descriptions ?? {}));
|
|
208
|
+
|
|
209
|
+
const classify = node => {
|
|
210
|
+
if (node.type !== 'object') {
|
|
211
|
+
return classifyPrimitive(node.type);
|
|
212
|
+
}
|
|
213
|
+
for (const contribution of contributions) {
|
|
214
|
+
const label = contribution.classify(node);
|
|
215
|
+
if (label !== undefined) {
|
|
216
|
+
return label;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return classifyObjectFallback(node);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const isAnchorConcept = label => anchorSet.has(label) || contributions.some(contribution => contribution.isAnchor?.(label) === true);
|
|
223
|
+
|
|
224
|
+
// The display group is structural: the first segment of the label path.
|
|
225
|
+
const conceptGroup = label => parseLabelPath(label)[0];
|
|
226
|
+
|
|
227
|
+
const describe = group => descriptions[group];
|
|
228
|
+
|
|
229
|
+
return { classify, isAnchorConcept, conceptGroup, grammarTypes, describe };
|
|
230
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* GLSP GModel concept classifier — a default contribution for heads with a
|
|
12
|
+
* `@hydranium/glsp-server` diagram layer. GModel elements are class instances
|
|
13
|
+
* from `@eclipse-glsp/graph` (GGraph/GNode/GEdge/GLabel/GCompartment/…), each
|
|
14
|
+
* carrying a `type` string and the shared own-prop set
|
|
15
|
+
* {type,id,children,cssClasses}. So this mirrors CST (recognise by constructor)
|
|
16
|
+
* and AST (sub-label by the discriminator) at once: a GModel instance →
|
|
17
|
+
* `GModel:<type>` (the field is `type`, NOT `$type`).
|
|
18
|
+
*
|
|
19
|
+
* The `type` string is itself hierarchical by GLSP convention
|
|
20
|
+
* (`<kind>:<subtype>`), so the label becomes a 3-level path
|
|
21
|
+
* `GModel:<kind>:<subtype>` that the analyzer drills into (group `GModel`, then
|
|
22
|
+
* by kind `node`/`edge`/`comp`/`label`/`graph`, then by subtype). A flat `type`
|
|
23
|
+
* (`graph`, `label`) is simply a 2-level leaf — never wrong.
|
|
24
|
+
*
|
|
25
|
+
* The element vocabulary is framework-fixed (`@eclipse-glsp/graph`), so it ships
|
|
26
|
+
* as a default rather than behind discovery. On a non-GLSP heap it matches
|
|
27
|
+
* nothing and is inert. Only leaf option bags (`layoutOptions`/`args`) are plain
|
|
28
|
+
* objects — negligible mass — so no plain-object predicate is needed; the
|
|
29
|
+
* structural fallback catches any GModel subtype a head adds beyond this set.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { has, propertySet, readableType } from './concepts.mjs';
|
|
33
|
+
|
|
34
|
+
/** `@eclipse-glsp/graph` element classes — the framework-fixed GModel vocabulary. */
|
|
35
|
+
const GMODEL_CTORS = new Set([
|
|
36
|
+
'GGraph',
|
|
37
|
+
'GNode',
|
|
38
|
+
'GEdge',
|
|
39
|
+
'GLabel',
|
|
40
|
+
'GCompartment',
|
|
41
|
+
'GPort',
|
|
42
|
+
'GForeignObjectElement',
|
|
43
|
+
'GHtmlRoot',
|
|
44
|
+
'GIssueMarker'
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** @type {import('./concepts.mjs').ConceptClassifier} */
|
|
48
|
+
export const glspClassifier = {
|
|
49
|
+
classify(node) {
|
|
50
|
+
if (node.type !== 'object') {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const { name } = node;
|
|
54
|
+
if (name && GMODEL_CTORS.has(name)) {
|
|
55
|
+
return `GModel:${readableType(node)}`;
|
|
56
|
+
}
|
|
57
|
+
// Recognise any GModel element by its shared own-prop shape
|
|
58
|
+
// {type,id,children,cssClasses}, regardless of constructor name. A bundled
|
|
59
|
+
// server renames the classes (`_GLabel`) and heads add their own
|
|
60
|
+
// subclasses (`_AttributeCompartment`), so a name allow-list misses most
|
|
61
|
+
// of a real diagram — the structural signature is what holds. The shape is
|
|
62
|
+
// GLSP-specific, and Langium runs first, so only unclassified objects reach
|
|
63
|
+
// this check (no risk of stealing a model node).
|
|
64
|
+
if (has(propertySet(node), 'type', 'id', 'children', 'cssClasses')) {
|
|
65
|
+
return `GModel:${readableType(node)}`;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Matches the raw per-node labels (exclusive-retained attribution anchors)
|
|
71
|
+
// and the bare group (`GModel`, retainer-path selection).
|
|
72
|
+
isAnchor(label) {
|
|
73
|
+
return label.startsWith('GModel');
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
descriptions: {
|
|
77
|
+
GModel:
|
|
78
|
+
'GLSP graphical-model elements for the open diagrams (nodes, edges, labels, ports, compartments). Sub-levels are the GLSP `type`, itself a `<kind>:<subtype>` path.'
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Generic Langium/LSP concept classifier — the default contribution every run
|
|
12
|
+
* loads. Its `classify(node)` returns a Langium concept label or `undefined` to
|
|
13
|
+
* defer to the next classifier / the engine fallback. Recognises class instances
|
|
14
|
+
* by constructor and plain objects by their property-name set; AST nodes and
|
|
15
|
+
* descriptions are sub-labelled by `$type` / element kind so the report can
|
|
16
|
+
* break down by element. No application-specific vocabulary — heads contribute
|
|
17
|
+
* their own classifier alongside this one.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { derefStringProp, has, propertySet, stringProp } from './concepts.mjs';
|
|
21
|
+
|
|
22
|
+
/** Concepts that own memory and act as exclusive-retained attribution anchors.
|
|
23
|
+
* (CST/AST node/AstNodeDescription are matched by prefix in `isAnchor`.) */
|
|
24
|
+
const ANCHOR_CONCEPTS = ['Range', 'Position', 'DocumentSegment', 'MultiMap', 'Map', 'LangiumDocument', 'ParseResult', 'Reference'];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* CST node constructors → their role, so CST drills down like AST
|
|
28
|
+
* (`CST:leaf`/`CST:composite`/`CST:root`/`CST:container`). Generic Langium names,
|
|
29
|
+
* no language coupling. A bundled server may prefix `_`, hence the lookup strips
|
|
30
|
+
* a leading underscore.
|
|
31
|
+
*/
|
|
32
|
+
const CST_ROLE = {
|
|
33
|
+
LeafCstNodeImpl: 'leaf',
|
|
34
|
+
CompositeCstNodeImpl: 'composite',
|
|
35
|
+
RootCstNodeImpl: 'root',
|
|
36
|
+
CstNodeContainer: 'container'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* AST node $types from Langium's OWN grammar language. These appear in a
|
|
41
|
+
* language-server heap (the parsed .langium grammar) but never in an adopter's
|
|
42
|
+
* document model, so a `--validate` cross-check treats them as expected
|
|
43
|
+
* snapshot-only rather than a discrepancy. Generic Langium, not app-specific.
|
|
44
|
+
*/
|
|
45
|
+
const LANGIUM_GRAMMAR_TYPES = [
|
|
46
|
+
'Grammar',
|
|
47
|
+
'GrammarImport',
|
|
48
|
+
'AbstractRule',
|
|
49
|
+
'ParserRule',
|
|
50
|
+
'TerminalRule',
|
|
51
|
+
'InfixRule',
|
|
52
|
+
'Type',
|
|
53
|
+
'Interface',
|
|
54
|
+
'AbstractType',
|
|
55
|
+
'Action',
|
|
56
|
+
'Alternatives',
|
|
57
|
+
'Group',
|
|
58
|
+
'UnorderedGroup',
|
|
59
|
+
'Assignment',
|
|
60
|
+
'RuleCall',
|
|
61
|
+
'CrossReference',
|
|
62
|
+
'Keyword',
|
|
63
|
+
'RegexToken',
|
|
64
|
+
'TerminalAlternatives',
|
|
65
|
+
'TerminalGroup',
|
|
66
|
+
'TerminalRuleCall',
|
|
67
|
+
'CharacterRange',
|
|
68
|
+
'NegatedToken',
|
|
69
|
+
'UntilToken',
|
|
70
|
+
'Wildcard',
|
|
71
|
+
'EndOfFile',
|
|
72
|
+
'SimpleType',
|
|
73
|
+
'ReferenceType',
|
|
74
|
+
'ArrayType',
|
|
75
|
+
'UnionType',
|
|
76
|
+
'TypeAttribute',
|
|
77
|
+
'ParameterReference',
|
|
78
|
+
'ReturnType',
|
|
79
|
+
'NamedArgument',
|
|
80
|
+
'Parameter',
|
|
81
|
+
'Conjunction',
|
|
82
|
+
'Disjunction',
|
|
83
|
+
'Negation',
|
|
84
|
+
'BooleanLiteral',
|
|
85
|
+
'NumberLiteral',
|
|
86
|
+
'StringLiteral'
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** @type {import('./concepts.mjs').ConceptClassifier} */
|
|
90
|
+
export const langiumClassifier = {
|
|
91
|
+
classify(node) {
|
|
92
|
+
const { type, name } = node;
|
|
93
|
+
if (type !== 'object') {
|
|
94
|
+
return undefined; // primitives are the engine's baseline
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Class instances: recognised by constructor name.
|
|
98
|
+
if (name && name !== 'Object') {
|
|
99
|
+
const cstRole = CST_ROLE[name.startsWith('_') ? name.slice(1) : name];
|
|
100
|
+
if (cstRole) {
|
|
101
|
+
// A leaf is a single token; sub-label by its token type
|
|
102
|
+
// (`_tokenType.name` — `ID`, `STRING`, or the keyword text), so the
|
|
103
|
+
// CST breakdown shows which tokens dominate (e.g. trivia vs identifiers).
|
|
104
|
+
if (cstRole === 'leaf') {
|
|
105
|
+
const token = derefStringProp(node, '_tokenType', 'name')?.trim();
|
|
106
|
+
if (!token) {
|
|
107
|
+
return 'CST:leaf';
|
|
108
|
+
}
|
|
109
|
+
// Token names are either identifier-like (terminals `ID`/`STRING`,
|
|
110
|
+
// keywords `name`/`value`) or punctuation (`:`/`{`/`;`). Bucket
|
|
111
|
+
// punctuation together: per-mark counts are noise, and a literal `:`
|
|
112
|
+
// would corrupt the `:`-delimited label path (and `>` the ` > `
|
|
113
|
+
// display) if embedded as a segment.
|
|
114
|
+
return /^[A-Za-z_][\w-]*$/.test(token) ? `CST:leaf:${token}` : 'CST:leaf:(punctuation)';
|
|
115
|
+
}
|
|
116
|
+
return `CST:${cstRole}`;
|
|
117
|
+
}
|
|
118
|
+
// Any class whose name ends in AstNodeDescription is a description wrapper;
|
|
119
|
+
// the prefix is the scope layer (Local, Global, ...). Labelled
|
|
120
|
+
// `AstNodeDescription:<layer>` so it nests under the description family at
|
|
121
|
+
// L2 (the bases sit at `AstNodeDescription:base:<kind>`). Generic: no app names.
|
|
122
|
+
if (name.endsWith('AstNodeDescription')) {
|
|
123
|
+
const layer = name.slice(0, -'AstNodeDescription'.length) || 'base';
|
|
124
|
+
return `AstNodeDescription:${layer}`;
|
|
125
|
+
}
|
|
126
|
+
// Index manager is an attribution anchor but keeps its real name; Map/MultiMap
|
|
127
|
+
// stay their own names too (referrer edges show which are the scope index).
|
|
128
|
+
if (name.endsWith('IndexManager') || name === 'MultiMap' || name === 'Map') {
|
|
129
|
+
return name;
|
|
130
|
+
}
|
|
131
|
+
return undefined; // defer (engine → class:<name>, or a later classifier)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Plain object literal: classify by property-name set.
|
|
135
|
+
const props = propertySet(node);
|
|
136
|
+
if (props.size === 0) {
|
|
137
|
+
// A Position {line,character} whose numeric fields are inline SMIs has NO
|
|
138
|
+
// property edges on the heap, so it looks empty. Recover it from an
|
|
139
|
+
// incoming Range `start`/`end` edge — otherwise the millions of Range
|
|
140
|
+
// endpoints misbucket as 'empty object {}' (the biggest single
|
|
141
|
+
// misclassification on a large workspace).
|
|
142
|
+
for (const edge of node.referrers) {
|
|
143
|
+
if (edge.type === 'property' && (edge.name_or_index === 'start' || edge.name_or_index === 'end')) {
|
|
144
|
+
return 'Position';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return undefined; // defer → engine 'empty object {}'
|
|
148
|
+
}
|
|
149
|
+
if (props.has('$type')) {
|
|
150
|
+
return `AST node:${stringProp(node, '$type') ?? 'unknown'}`;
|
|
151
|
+
}
|
|
152
|
+
if (props.size === 2 && has(props, 'start', 'end')) {
|
|
153
|
+
return 'Range';
|
|
154
|
+
}
|
|
155
|
+
if (has(props, 'line', 'character')) {
|
|
156
|
+
return 'Position';
|
|
157
|
+
}
|
|
158
|
+
if (has(props, 'parseResult', 'uri') || has(props, 'localSymbols', 'parseResult', 'uri')) {
|
|
159
|
+
return 'LangiumDocument';
|
|
160
|
+
}
|
|
161
|
+
if (has(props, 'value', 'lexerErrors', 'parserErrors')) {
|
|
162
|
+
return 'ParseResult';
|
|
163
|
+
}
|
|
164
|
+
if (has(props, 'name', 'nameSegment', 'documentUri', 'node', 'selectionSegment')) {
|
|
165
|
+
// The base description payload carries `type` = the described node's element
|
|
166
|
+
// kind. Labelled `AstNodeDescription:base:<kind>` so it nests under the
|
|
167
|
+
// description family: L2 `base` aggregates all payloads (alongside the L2
|
|
168
|
+
// scope-wrapper layers above), L3 `<kind>` is the per-element-kind split.
|
|
169
|
+
return `AstNodeDescription:base:${stringProp(node, 'type') ?? 'unknown'}`;
|
|
170
|
+
}
|
|
171
|
+
if (has(props, '$refText', 'ref')) {
|
|
172
|
+
return 'Reference';
|
|
173
|
+
}
|
|
174
|
+
if (has(props, 'range') && props.size <= 3) {
|
|
175
|
+
return 'DocumentSegment';
|
|
176
|
+
}
|
|
177
|
+
return undefined; // defer → engine 'other Object shape'
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
anchorConcepts: ANCHOR_CONCEPTS,
|
|
181
|
+
grammarTypes: LANGIUM_GRAMMAR_TYPES,
|
|
182
|
+
|
|
183
|
+
// Matches both the raw per-node labels (`AST node:Class`, used as exclusive-
|
|
184
|
+
// retained attribution anchors) and the bare group (`AST node`, used to pick
|
|
185
|
+
// top concepts for the retainer-path sample). `AstNodeDescription` prefix
|
|
186
|
+
// already covers the group and every layer/base path.
|
|
187
|
+
isAnchor(label) {
|
|
188
|
+
return (
|
|
189
|
+
label.startsWith('AST node') || label.startsWith('AstNodeDescription') || label.startsWith('CST') || label.endsWith('IndexManager')
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
descriptions: {
|
|
194
|
+
'AST node':
|
|
195
|
+
'Parsed abstract-syntax-tree nodes, split by `$type` (includes the Langium grammar AST since the parsed grammar shares the heap).',
|
|
196
|
+
AstNodeDescription:
|
|
197
|
+
'Scope-index entries. `base` is the payload (one per exported symbol, split by element kind); `Local`/`Global`/… are thin per-scope-layer wrappers that delegate to a base.',
|
|
198
|
+
CST: 'Concrete-syntax-tree nodes: `leaf` = a token (sub-split by token type — `ID`, `STRING`, keyword text, with all punctuation bucketed as `(punctuation)`), `composite` = a grammar rule, `root` = a document root, `container` = a child-node array.',
|
|
199
|
+
Range: 'LSP text ranges (`start`/`end` Position pairs) attached to CST nodes and document segments.',
|
|
200
|
+
Position:
|
|
201
|
+
'LSP `{line, character}` endpoints of Ranges (their numeric fields are inline, so they carry no heap properties of their own).'
|
|
202
|
+
}
|
|
203
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Renderer (browser) heap classification — the `--renderer` mode's counterpart to
|
|
12
|
+
* the Langium/GLSP classifiers. A renderer `.heapsnapshot` (from CDP
|
|
13
|
+
* `HeapProfiler.takeHeapSnapshot`, the `browser-heap` artefact) has NO Langium
|
|
14
|
+
* structure; its leak vocabulary is the DOM: detached DOM subtrees and the JS
|
|
15
|
+
* that retains them (event listeners, closures, arrays). So this classifier
|
|
16
|
+
* buckets by that vocabulary and lets the SAME report machinery (exclusive
|
|
17
|
+
* retained, holders, retainer paths) do the work — a retainer path to a
|
|
18
|
+
* `Detached DOM` concept IS the listener-retainer analysis.
|
|
19
|
+
*
|
|
20
|
+
* Unlike a normal ConceptClassifier contribution, this cannot be composed through
|
|
21
|
+
* `composeClassifiers`: DOM nodes are `native`-typed, and the engine short-circuits
|
|
22
|
+
* every non-`object` node to its primitive bucket BEFORE contributions run. So the
|
|
23
|
+
* renderer mode installs a dedicated `classify` that intercepts `native`/DOM nodes
|
|
24
|
+
* itself and delegates everything else to the engine baseline (the JS/V8 universals
|
|
25
|
+
* — strings, arrays, closures — which a renderer heap has just like any other).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { composeClassifiers } from '../langium/concepts.mjs';
|
|
29
|
+
|
|
30
|
+
const DETACHED_PREFIX = 'Detached ';
|
|
31
|
+
const SYSTEM_PREFIX = 'system';
|
|
32
|
+
/** Object-node class names V8 uses for a registered DOM event listener. */
|
|
33
|
+
const EVENT_LISTENER_NAMES = new Set(['V8EventListener', 'EventListener']);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A clean, single-segment DOM key for a heap node name. V8 names a DOM element by
|
|
37
|
+
* its markup or by its class (`HTMLDivElement`, `Text`). The raw markup breaks the
|
|
38
|
+
* report: the `<…>` is eaten as literal HTML by a Markdown renderer, an inline
|
|
39
|
+
* `style` attribute's `:` collides with the label-path separator, and a `|` would
|
|
40
|
+
* break the table column. So an element descriptor is normalised to a CSS selector
|
|
41
|
+
* — dropping the `style`/`data-*` soup, which also collapses near-duplicate rows —
|
|
42
|
+
* and any residual `:`/`|`/`<`/`>`/`"` is stripped. A plain class name passes
|
|
43
|
+
* through, and V8's trailing ` / <url>` suffix is dropped.
|
|
44
|
+
*/
|
|
45
|
+
function domClass(name) {
|
|
46
|
+
const raw = (name || 'native').split(' / ')[0].trim() || 'native';
|
|
47
|
+
const element = raw.match(/^<([a-zA-Z][\w-]*)([^>]*)>$/);
|
|
48
|
+
let key = raw;
|
|
49
|
+
if (element) {
|
|
50
|
+
const tag = element[1].toLowerCase();
|
|
51
|
+
const id = (element[2].match(/\bid="([^"]*)"/) ?? ['', ''])[1].trim();
|
|
52
|
+
const classes = (element[2].match(/\bclass="([^"]*)"/) ?? ['', ''])[1].trim();
|
|
53
|
+
const idSel = id ? `#${id}` : '';
|
|
54
|
+
const classSel = classes ? `.${classes.split(/\s+/).filter(Boolean).join('.')}` : '';
|
|
55
|
+
key = `${tag}${idSel}${classSel}`;
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
key
|
|
59
|
+
.replace(/[:|<>"]/g, ' ')
|
|
60
|
+
.replace(/\s+/g, ' ')
|
|
61
|
+
.trim()
|
|
62
|
+
.slice(0, 80) || 'native'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The subtype of a V8 `system` native (`system / Context` → `Context`; bare `system` → `(other)`). */
|
|
67
|
+
function nativeSubtype(name) {
|
|
68
|
+
const slash = name.indexOf('/');
|
|
69
|
+
return slash >= 0 ? name.slice(slash + 1).trim() || '(other)' : '(other)';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const RENDERER_DESCRIPTIONS = {
|
|
73
|
+
'Detached DOM':
|
|
74
|
+
"DOM nodes no longer in the document tree but still retained by JS — the classic browser leak. Follow each concept's retainer path to the listener / closure / array holding it; that holder is the lever.",
|
|
75
|
+
'DOM node':
|
|
76
|
+
'Live DOM nodes still attached to the document, grouped by element class. Growth across a session (vs a stable baseline) points at accumulating UI.',
|
|
77
|
+
'V8 native':
|
|
78
|
+
'V8-internal native allocations (`system / Context`, `system / JSArrayBufferData`, …) — NOT DOM. Renderer memory held by the engine (contexts, ArrayBuffer backing stores), grouped by system subtype.',
|
|
79
|
+
'Event listener':
|
|
80
|
+
'Registered DOM event listeners (V8EventListener). A listener retaining a detached subtree is a leak; a rising count is the other classic tell.'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build the renderer classification functions in the shape the analyzer consumes
|
|
85
|
+
* ({@link composeClassifiers}'s return), intercepting `native`/DOM + event-listener
|
|
86
|
+
* nodes and delegating the rest to the engine baseline (no Langium/GLSP vocabulary).
|
|
87
|
+
*/
|
|
88
|
+
export function rendererClassifiers() {
|
|
89
|
+
const baseline = composeClassifiers([]);
|
|
90
|
+
const classify = node => {
|
|
91
|
+
if (node.type === 'native') {
|
|
92
|
+
const name = node.name || '';
|
|
93
|
+
if (name.startsWith(DETACHED_PREFIX)) {
|
|
94
|
+
return `Detached DOM:${domClass(name.slice(DETACHED_PREFIX.length))}`;
|
|
95
|
+
}
|
|
96
|
+
// V8-internal native allocations (`system / Context`, `system / JSArrayBufferData`,
|
|
97
|
+
// …) are NOT DOM — bucket them apart so `DOM node` reflects real elements.
|
|
98
|
+
if (name === SYSTEM_PREFIX || name.startsWith(`${SYSTEM_PREFIX} `) || name.startsWith(`${SYSTEM_PREFIX}/`)) {
|
|
99
|
+
return `V8 native:${nativeSubtype(name)}`;
|
|
100
|
+
}
|
|
101
|
+
return `DOM node:${domClass(name)}`;
|
|
102
|
+
}
|
|
103
|
+
if (node.type === 'object' && EVENT_LISTENER_NAMES.has(node.name)) {
|
|
104
|
+
return 'Event listener';
|
|
105
|
+
}
|
|
106
|
+
return baseline.classify(node);
|
|
107
|
+
};
|
|
108
|
+
// Anchor the DOM concepts AND the per-constructor `class:<Name>` buckets: a renderer
|
|
109
|
+
// heap has no curated model, so class instances (widgets, models, caches, framework
|
|
110
|
+
// objects) are the meaningful attribution anchors. Without this the ~1/5 of the heap
|
|
111
|
+
// that is the JS object graph shows 0 exclusive retained (attributed up to GC roots);
|
|
112
|
+
// anchoring per-class turns it into a "which constructor retains the most" view.
|
|
113
|
+
const isAnchorConcept = label =>
|
|
114
|
+
label.startsWith('Detached DOM') ||
|
|
115
|
+
label.startsWith('DOM node') ||
|
|
116
|
+
label.startsWith('V8 native') ||
|
|
117
|
+
label === 'Event listener' ||
|
|
118
|
+
label === 'class' ||
|
|
119
|
+
label.startsWith('class:');
|
|
120
|
+
const describe = group => RENDERER_DESCRIPTIONS[group];
|
|
121
|
+
return { classify, isAnchorConcept, conceptGroup: baseline.conceptGroup, grammarTypes: new Set(), describe };
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
/**
|
|
10
|
+
* The version of the running binary.
|
|
11
|
+
*
|
|
12
|
+
* Read from the manifest rather than emitted as a constant at build time: a
|
|
13
|
+
* generated constant is a second copy of a number the manifest already carries,
|
|
14
|
+
* and nothing goes red while the two disagree.
|
|
15
|
+
*
|
|
16
|
+
* Throws rather than reporting `unknown`: every caller turns the number into a
|
|
17
|
+
* claim someone else acts on — a pinned dependency range, a version in a bug
|
|
18
|
+
* report — where a placeholder is worse than a failure.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readCliVersion(): string;
|
|
21
|
+
//# sourceMappingURL=cli-version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-version.d.ts","sourceRoot":"","sources":["../src/cli-version.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAOlF;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAMvC"}
|