arkanalyzer 1.0.90 → 1.0.91

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.
Files changed (122) hide show
  1. package/config/arkanalyzer.json +0 -1
  2. package/lib/Config.d.ts +3 -0
  3. package/lib/Config.d.ts.map +1 -1
  4. package/lib/Scene.d.ts +99 -0
  5. package/lib/Scene.d.ts.map +1 -1
  6. package/lib/Scene.js +229 -13
  7. package/lib/callgraph/common/Statistics.js +2 -2
  8. package/lib/callgraph/model/CallGraph.d.ts +2 -2
  9. package/lib/callgraph/model/CallGraph.d.ts.map +1 -1
  10. package/lib/callgraph/model/CallGraph.js +6 -6
  11. package/lib/callgraph/model/CallSite.d.ts +1 -1
  12. package/lib/callgraph/model/CallSite.d.ts.map +1 -1
  13. package/lib/callgraph/model/CallSite.js +2 -2
  14. package/lib/core/base/Ref.d.ts.map +1 -1
  15. package/lib/core/base/Ref.js +6 -1
  16. package/lib/core/common/ArkIRTransformer.d.ts.map +1 -1
  17. package/lib/core/common/ArkIRTransformer.js +5 -0
  18. package/lib/core/common/ArkValueTransformer.d.ts.map +1 -1
  19. package/lib/core/common/ArkValueTransformer.js +2 -1
  20. package/lib/core/common/Const.d.ts +3 -0
  21. package/lib/core/common/Const.d.ts.map +1 -1
  22. package/lib/core/common/Const.js +5 -1
  23. package/lib/core/common/DummyMainCreater.d.ts +25 -12
  24. package/lib/core/common/DummyMainCreater.d.ts.map +1 -1
  25. package/lib/core/common/DummyMainCreater.js +90 -76
  26. package/lib/core/common/EtsConst.d.ts +2 -0
  27. package/lib/core/common/EtsConst.d.ts.map +1 -1
  28. package/lib/core/common/EtsConst.js +133 -125
  29. package/lib/core/common/SdkUtils.d.ts +1 -0
  30. package/lib/core/common/SdkUtils.d.ts.map +1 -1
  31. package/lib/core/common/SdkUtils.js +29 -0
  32. package/lib/core/graph/BaseExplicitGraph.d.ts +2 -0
  33. package/lib/core/graph/BaseExplicitGraph.d.ts.map +1 -1
  34. package/lib/core/graph/BaseExplicitGraph.js +12 -1
  35. package/lib/core/graph/BaseImplicitGraph.d.ts +16 -0
  36. package/lib/core/graph/BaseImplicitGraph.d.ts.map +1 -1
  37. package/lib/core/graph/BaseImplicitGraph.js +21 -0
  38. package/lib/core/graph/FileDepGraph.d.ts +75 -0
  39. package/lib/core/graph/FileDepGraph.d.ts.map +1 -0
  40. package/lib/core/graph/FileDepGraph.js +170 -0
  41. package/lib/core/graph/GraphTraits.d.ts +2 -0
  42. package/lib/core/graph/GraphTraits.d.ts.map +1 -1
  43. package/lib/core/graph/GraphTraits.js +1 -1
  44. package/lib/core/graph/ModuleDepGraph.d.ts +175 -0
  45. package/lib/core/graph/ModuleDepGraph.d.ts.map +1 -0
  46. package/lib/core/graph/ModuleDepGraph.js +421 -0
  47. package/lib/core/graph/Scc.d.ts +1 -3
  48. package/lib/core/graph/Scc.d.ts.map +1 -1
  49. package/lib/core/graph/Scc.js +6 -19
  50. package/lib/core/graph/builder/CfgBuilder.d.ts.map +1 -1
  51. package/lib/core/graph/builder/CfgBuilder.js +7 -1
  52. package/lib/core/graph/builder/TrapBuilder.d.ts +1 -0
  53. package/lib/core/graph/builder/TrapBuilder.d.ts.map +1 -1
  54. package/lib/core/graph/builder/TrapBuilder.js +27 -7
  55. package/lib/core/model/ArkBody.d.ts +1 -0
  56. package/lib/core/model/ArkBody.d.ts.map +1 -1
  57. package/lib/core/model/ArkBody.js +7 -0
  58. package/lib/core/model/ArkClass.d.ts +1 -0
  59. package/lib/core/model/ArkClass.d.ts.map +1 -1
  60. package/lib/core/model/ArkClass.js +37 -0
  61. package/lib/core/model/ArkExport.d.ts +1 -0
  62. package/lib/core/model/ArkExport.d.ts.map +1 -1
  63. package/lib/core/model/ArkExport.js +6 -0
  64. package/lib/core/model/ArkField.d.ts +1 -0
  65. package/lib/core/model/ArkField.d.ts.map +1 -1
  66. package/lib/core/model/ArkField.js +5 -0
  67. package/lib/core/model/ArkFile.d.ts +5 -0
  68. package/lib/core/model/ArkFile.d.ts.map +1 -1
  69. package/lib/core/model/ArkFile.js +30 -0
  70. package/lib/core/model/ArkImport.d.ts +1 -0
  71. package/lib/core/model/ArkImport.d.ts.map +1 -1
  72. package/lib/core/model/ArkImport.js +5 -0
  73. package/lib/core/model/ArkMethod.d.ts +8 -0
  74. package/lib/core/model/ArkMethod.d.ts.map +1 -1
  75. package/lib/core/model/ArkMethod.js +22 -0
  76. package/lib/core/model/ArkModule.d.ts +136 -0
  77. package/lib/core/model/ArkModule.d.ts.map +1 -0
  78. package/lib/core/model/ArkModule.js +250 -0
  79. package/lib/core/model/ArkNamespace.d.ts +1 -0
  80. package/lib/core/model/ArkNamespace.d.ts.map +1 -1
  81. package/lib/core/model/ArkNamespace.js +18 -0
  82. package/lib/core/model/builder/ArkFileBuilder.d.ts +19 -0
  83. package/lib/core/model/builder/ArkFileBuilder.d.ts.map +1 -1
  84. package/lib/core/model/builder/ArkFileBuilder.js +155 -51
  85. package/lib/frontend/FrontendBuilder.d.ts +116 -0
  86. package/lib/frontend/FrontendBuilder.d.ts.map +1 -1
  87. package/lib/frontend/FrontendBuilder.js +256 -0
  88. package/lib/frontend/arktsFrontend/ArktsFrontend.d.ts +11 -0
  89. package/lib/frontend/arktsFrontend/ArktsFrontend.d.ts.map +1 -1
  90. package/lib/frontend/arktsFrontend/ArktsFrontend.js +15 -0
  91. package/lib/frontend/common/MemoryMonitor.d.ts +62 -0
  92. package/lib/frontend/common/MemoryMonitor.d.ts.map +1 -0
  93. package/lib/frontend/common/MemoryMonitor.js +108 -0
  94. package/lib/frontend/common/ModuleAnalysisConfig.d.ts +92 -0
  95. package/lib/frontend/common/ModuleAnalysisConfig.d.ts.map +1 -0
  96. package/lib/frontend/common/ModuleAnalysisConfig.js +144 -0
  97. package/lib/frontend/common/ModuleBuilder.d.ts +497 -0
  98. package/lib/frontend/common/ModuleBuilder.d.ts.map +1 -0
  99. package/lib/frontend/common/ModuleBuilder.js +1458 -0
  100. package/lib/frontend/common/ModuleCache.d.ts +119 -0
  101. package/lib/frontend/common/ModuleCache.d.ts.map +1 -0
  102. package/lib/frontend/common/ModuleCache.js +197 -0
  103. package/lib/frontend/common/ModuleDepth.d.ts +28 -0
  104. package/lib/frontend/common/ModuleDepth.d.ts.map +1 -0
  105. package/lib/frontend/common/ModuleDepth.js +45 -0
  106. package/lib/frontend/cppFrontend/CppFrontend.d.ts +5 -0
  107. package/lib/frontend/cppFrontend/CppFrontend.d.ts.map +1 -1
  108. package/lib/frontend/cppFrontend/CppFrontend.js +11 -1
  109. package/lib/frontend/cppFrontend/model/builder/ArkFileBuilder.d.ts +8 -0
  110. package/lib/frontend/cppFrontend/model/builder/ArkFileBuilder.d.ts.map +1 -1
  111. package/lib/frontend/cppFrontend/model/builder/ArkFileBuilder.js +105 -2
  112. package/lib/index.d.ts +9 -1
  113. package/lib/index.d.ts.map +1 -1
  114. package/lib/index.js +22 -3
  115. package/lib/utils/Canonicalizer.d.ts +105 -0
  116. package/lib/utils/Canonicalizer.d.ts.map +1 -0
  117. package/lib/utils/Canonicalizer.js +520 -0
  118. package/lib/utils/ModuleUtils.js +4 -2
  119. package/lib/utils/entryMethodUtils.d.ts +23 -0
  120. package/lib/utils/entryMethodUtils.d.ts.map +1 -1
  121. package/lib/utils/entryMethodUtils.js +53 -1
  122. package/package.json +1 -1
@@ -0,0 +1,1458 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Huawei Device Co., Ltd.
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.ModuleBuilder = void 0;
44
+ const fs_1 = __importDefault(require("fs"));
45
+ const path_1 = __importDefault(require("path"));
46
+ const SparseBitVector_1 = require("../../utils/SparseBitVector");
47
+ const Canonicalizer_1 = require("../../utils/Canonicalizer");
48
+ const ArkModule_1 = require("../../core/model/ArkModule");
49
+ const ModuleDepGraph_1 = require("../../core/graph/ModuleDepGraph");
50
+ const FileDepGraph_1 = require("../../core/graph/FileDepGraph");
51
+ const EtsConst_1 = require("../../core/common/EtsConst");
52
+ const Const_1 = require("../../core/common/Const");
53
+ const json5parser_1 = require("../../utils/json5parser");
54
+ const logger_1 = __importStar(require("../../utils/logger"));
55
+ const ModuleDepth_1 = require("./ModuleDepth");
56
+ const MemoryMonitor_1 = require("./MemoryMonitor");
57
+ const ModuleCache_1 = require("./ModuleCache");
58
+ const FrontendBuilder_1 = require("../FrontendBuilder");
59
+ const ArkFile_1 = require("../../core/model/ArkFile");
60
+ const Inference_1 = require("../../core/inference/Inference");
61
+ const FileUtils_1 = require("../../utils/FileUtils");
62
+ const getAllFiles_1 = require("../../utils/getAllFiles");
63
+ const SdkUtils_1 = require("../../core/common/SdkUtils");
64
+ const ArktsFrontend_1 = require("../arktsFrontend/ArktsFrontend");
65
+ const ModelUtils_1 = require("../../core/common/ModelUtils");
66
+ const ModuleUtils_1 = require("../../utils/ModuleUtils");
67
+ const Scene_1 = require("../../Scene");
68
+ const logger = logger_1.default.getLogger(logger_1.LOG_MODULE_TYPE.ARKANALYZER, 'ModuleBuilder');
69
+ /**
70
+ * ModuleBuilder holds the build logic for module-level analysis embedded in {@link Scene}.
71
+ *
72
+ * It is responsible for the build side of the module lifecycle: registration, SDK build, module
73
+ * preparation, dependency graph construction and SCC analysis, and module loading. Persistent
74
+ * state (the Canonicalizer, pathToId map, dependency graph, and progress flags) lives on the
75
+ * owning {@link Scene}; this class reads and writes that state through the Scene accessors.
76
+ *
77
+ * Each module is identified by its absolute path and assigned a dense integer {@link ModuleID} via
78
+ * the Scene's Canonicalizer using the objectIdentity strategy. The `pathToId` map on the Scene is
79
+ * a persistent cache that survives graph construction, allowing runtime path-based lookups.
80
+ *
81
+ * @category core/model
82
+ */
83
+ class ModuleBuilder {
84
+ constructor(scene) {
85
+ /** Persistent map from absolute module path to ModuleID (survives graph construction). */
86
+ this.pathToId = new Map();
87
+ /** Topological order of the current call, used for protected-set computation and Phase 2/3 reverse-topo iteration. */
88
+ this.topoOrder = [];
89
+ /** Load level to which protected dependency modules are downgraded during eviction Phase 2. */
90
+ this.dependencyLoadLevel = ModuleDepth_1.ModuleDepthLevel.SIGNATURES;
91
+ this.scene = scene;
92
+ // Initialize pathToId from existing modules (supports idempotent calls)
93
+ for (const module of scene.getModules()) {
94
+ this.pathToId.set(module.getModulePath(), scene.getModuleId(module));
95
+ }
96
+ }
97
+ // --- Module query methods (moved from Scene) ---
98
+ /** Get a registered module by its ModuleID. Returns undefined when the id is out of bounds. */
99
+ getModule(id) {
100
+ return this.scene.getModule(id);
101
+ }
102
+ /** Look up a module by its absolute path using the pathToId cache. */
103
+ getModuleByPath(modulePath) {
104
+ const id = this.pathToId.get(modulePath);
105
+ if (id === undefined) {
106
+ return undefined;
107
+ }
108
+ return this.scene.getModule(id);
109
+ }
110
+ /** Total number of registered modules. */
111
+ getModuleCount() {
112
+ return this.scene.getModuleCount();
113
+ }
114
+ /**
115
+ * Iterate over all registered modules.
116
+ * Iteration order follows ModuleID assignment order (0, 1, 2, ...).
117
+ */
118
+ modulesIterator() {
119
+ const cursor = { value: 0 };
120
+ const iterator = {
121
+ next: () => this.nextModule(cursor),
122
+ [Symbol.iterator]() {
123
+ return this;
124
+ },
125
+ };
126
+ return iterator;
127
+ }
128
+ /** Find the next module starting from the cursor. */
129
+ nextModule(cursor) {
130
+ while (cursor.value < this.scene.getModuleCount()) {
131
+ const m = this.scene.getModule(cursor.value);
132
+ cursor.value++;
133
+ if (m) {
134
+ return { value: m, done: false };
135
+ }
136
+ }
137
+ return { value: undefined, done: true };
138
+ }
139
+ /** Topologically sorted module IDs (empty before dependency analysis completes). */
140
+ getTopoOrder() {
141
+ const graph = this.scene.getModuleDepGraph();
142
+ return graph ? graph.getTopoOrder() : [];
143
+ }
144
+ /**
145
+ * Resolve a dependency alias within the scope of the given module.
146
+ * Looks up the alias in the module's per-module alias map, then resolves the
147
+ * resulting ModuleID back to the depended-on ArkModule.
148
+ */
149
+ resolveAlias(moduleId, alias) {
150
+ const module = this.getModule(moduleId);
151
+ if (!module) {
152
+ return undefined;
153
+ }
154
+ const depId = module.resolveDependencyAlias(alias);
155
+ if (depId === undefined) {
156
+ return undefined;
157
+ }
158
+ return this.getModule(depId);
159
+ }
160
+ /**
161
+ * Resolve the final set of target module IDs from the config's three selection dimensions.
162
+ * Excluded IDs take precedence; type filter and explicit include IDs are unioned.
163
+ */
164
+ resolveTargetModuleIds(config) {
165
+ const targets = new SparseBitVector_1.SparseBitVector();
166
+ const excluded = config.getExcludedModuleIds();
167
+ const included = config.getTargetModuleIds();
168
+ for (const module of this.modulesIterator()) {
169
+ const id = this.scene.getModuleId(module);
170
+ if (excluded.test(id)) {
171
+ continue;
172
+ }
173
+ if (config.isTypeIncluded(module.getModuleType()) || included.test(id)) {
174
+ targets.set(id);
175
+ }
176
+ }
177
+ return targets;
178
+ }
179
+ /**
180
+ * Compute transitive closure of module IDs reachable from the given target IDs via
181
+ * dependency edges (BFS over {@link ModuleDepGraph.getSuccModuleIds}).
182
+ * SDK modules are never included in the closure.
183
+ */
184
+ computeModuleClosureByIds(targetIds) {
185
+ const closure = new SparseBitVector_1.SparseBitVector();
186
+ const queue = [];
187
+ for (const id of targetIds) {
188
+ const module = this.scene.getModule(id);
189
+ if (!module || module.getModuleType() === ArkModule_1.ModuleType.SDK) {
190
+ continue;
191
+ }
192
+ if (!closure.test(id)) {
193
+ closure.set(id);
194
+ queue.push(id);
195
+ }
196
+ }
197
+ while (queue.length > 0) {
198
+ const currentId = queue.shift();
199
+ const graph = this.scene.getModuleDepGraph();
200
+ const succIds = graph ? graph.getSuccModuleIds(currentId) : [];
201
+ for (const depId of succIds) {
202
+ if (closure.test(depId)) {
203
+ continue;
204
+ }
205
+ const depModule = this.scene.getModule(depId);
206
+ if (!depModule || depModule.getModuleType() === ArkModule_1.ModuleType.SDK) {
207
+ continue;
208
+ }
209
+ closure.set(depId);
210
+ queue.push(depId);
211
+ }
212
+ }
213
+ return closure;
214
+ }
215
+ /** Filter the topological order to only include module IDs present in the given closure. */
216
+ getFilteredTopoOrder(closure) {
217
+ const graph = this.scene.getModuleDepGraph();
218
+ const topo = graph ? graph.getTopoOrder() : [];
219
+ return topo.filter(id => closure.test(id));
220
+ }
221
+ /** Get the ModuleID assigned to a registered ArkModule. */
222
+ getModuleId(module) {
223
+ return this.scene.getModuleId(module);
224
+ }
225
+ /** Create a {@link ModuleDepGraph} sharing the scene's module canonicalizer. */
226
+ createModuleDepGraph() {
227
+ return this.scene.createModuleDepGraph();
228
+ }
229
+ // --- Module registration ---
230
+ /**
231
+ * Register a module by its absolute path. If the same path has already been registered, the
232
+ * existing {@link ArkModule} is returned unchanged. A new ModuleID is allocated via the
233
+ * Scene's Canonicalizer for first-time registrations, and cached on the module.
234
+ *
235
+ * @param modulePath - Absolute path of the module (primary identifier).
236
+ * @param moduleName - Optional module name (auxiliary field, e.g. "@ohos/entry").
237
+ * @returns The registered ArkModule (newly created or previously registered).
238
+ */
239
+ registerModule(modulePath, moduleName = '') {
240
+ const existingId = this.pathToId.get(modulePath);
241
+ if (existingId !== undefined) {
242
+ return this.scene.getModule(existingId);
243
+ }
244
+ const module = new ArkModule_1.ArkModule(this.scene);
245
+ module.setModulePath(modulePath);
246
+ module.setModuleName(moduleName);
247
+ const id = this.scene.getModuleId(module);
248
+ this.pathToId.set(modulePath, id);
249
+ return module;
250
+ }
251
+ // --- SDK and module preparation ---
252
+ /**
253
+ * Register and build SDK modules in one fused step. Each project-level SDK is treated as an
254
+ * {@link ArkModule} (moduleType=SDK): its files are collected, parsed, and registered into the
255
+ * module's `filesMap` as well as the scene's `sdkArkFilesMap`. SDK type inference and global
256
+ * API merge run after all SDK files are built.
257
+ *
258
+ * This method merges the former `prepareSdkModules` (registration-only) and `SDKBuilder.buildSdks`
259
+ * (building-only) into a single step — SDK modules are never just registered without being built.
260
+ *
261
+ * Module-level SDKs (those with `moduleName` set) are skipped entirely.
262
+ *
263
+ * Idempotent: guarded by {@link SceneBuildStage.SDK_INFERRED}.
264
+ */
265
+ buildSdkModules() {
266
+ if (this.scene.getBuildStage() >= Scene_1.SceneBuildStage.SDK_INFERRED) {
267
+ return;
268
+ }
269
+ this.setEsVersionFromBuildProfile();
270
+ const sceneConfig = this.scene.getSceneConfig();
271
+ if (!sceneConfig) {
272
+ logger.warn('SceneConfig is not set; skip SDK build.');
273
+ return;
274
+ }
275
+ const sdks = sceneConfig.getSdksObj();
276
+ const options = this.scene.getOptions();
277
+ // Handle enableBuiltIn: prepend built-in SDK if not already present
278
+ if (options.enableBuiltIn && !sdks.find(sdk => sdk.name === SdkUtils_1.SdkUtils.BUILT_IN_NAME)) {
279
+ sdks.unshift(SdkUtils_1.SdkUtils.getBuiltInSdk());
280
+ }
281
+ // Register and build each SDK as a module (fused registration + building).
282
+ // moduleName is ignored — all SDKs are treated as modules regardless.
283
+ for (const sdk of sdks) {
284
+ const sdkPath = path_1.default.normalize(sdk.path);
285
+ const module = this.registerModule(sdkPath, sdk.name);
286
+ module.setModuleType(ArkModule_1.ModuleType.SDK);
287
+ this.buildSdkModuleFiles(module, sdk.name, sdkPath);
288
+ this.scene.getProjectSdkMap().set(sdk.name, sdk);
289
+ }
290
+ // SDK type inference + global API merge
291
+ const sdkArkFiles = this.scene.getSdkArkFiles();
292
+ const sdkGlobalMap = this.scene.getSdkGlobalMap();
293
+ for (const file of sdkArkFiles) {
294
+ if (file.getLanguage() === ArkFile_1.Language.CXX) {
295
+ continue;
296
+ }
297
+ Inference_1.InferenceManager.getInstance().getInference(file.getLanguage()).doInfer(file);
298
+ SdkUtils_1.SdkUtils.mergeGlobalAPI(file, sdkGlobalMap);
299
+ }
300
+ for (const file of sdkArkFiles) {
301
+ SdkUtils_1.SdkUtils.postInferredSdk(file, sdkGlobalMap);
302
+ }
303
+ this.scene.setBuildStage(Scene_1.SceneBuildStage.SDK_INFERRED);
304
+ SdkUtils_1.SdkUtils.extendArkUI(this.scene);
305
+ }
306
+ /**
307
+ * Collect and build all source files for a single SDK module. Each file is parsed via
308
+ * {@link ArktsFrontend.buildArkFileFromSdkPath} (using the SDK name as projectName), default
309
+ * method bodies are built and all BodyBuilders freed, then the file is registered into both
310
+ * the module's `filesMap` and the scene's `sdkArkFilesMap`. C++ SDK files are skipped.
311
+ */
312
+ buildSdkModuleFiles(module, sdkName, sdkPath) {
313
+ const allFiles = this.collectSdkFiles(sdkName, sdkPath);
314
+ for (const file of allFiles) {
315
+ if (FileUtils_1.FileUtils.getFileLanguage(file, this.scene.getFileLanguages()) === ArkFile_1.Language.CXX) {
316
+ continue;
317
+ }
318
+ this.parseAndRegisterSdkFile(module, file, sdkPath, sdkName);
319
+ }
320
+ }
321
+ /**
322
+ * Collect source file paths for an SDK.
323
+ *
324
+ * Built-in SDK uses {@link SdkUtils.fetchBuiltInFiles} (reference-based DFS over `lib.*.d.ts`);
325
+ * other SDKs scan the directory with the scene's supported extensions and ignore patterns.
326
+ */
327
+ collectSdkFiles(sdkName, sdkPath) {
328
+ var _a, _b, _c;
329
+ if (sdkName === SdkUtils_1.SdkUtils.BUILT_IN_NAME) {
330
+ const builtInFiles = SdkUtils_1.SdkUtils.fetchBuiltInFiles(sdkPath);
331
+ if (builtInFiles.length > 0) {
332
+ (_a = this.scene.getOptions().sdkGlobalFolders) === null || _a === void 0 ? void 0 : _a.push(sdkPath);
333
+ }
334
+ return builtInFiles;
335
+ }
336
+ SdkUtils_1.SdkUtils.loadSystemComponentsFromSdk(sdkPath);
337
+ const options = this.scene.getOptions();
338
+ const supportFileExts = (_b = options.supportFileExts) !== null && _b !== void 0 ? _b : ['.ets', '.ts'];
339
+ const ignoreFileNames = (_c = options.ignoreFileNames) !== null && _c !== void 0 ? _c : [];
340
+ return (0, getAllFiles_1.getAllFiles)(sdkPath, supportFileExts, ignoreFileNames);
341
+ }
342
+ /**
343
+ * Parse a single SDK source file into an ArkFile, build default method bodies, free all
344
+ * BodyBuilders, and register the file in both the module's `filesMap` and the scene's
345
+ * `sdkArkFilesMap`.
346
+ */
347
+ parseAndRegisterSdkFile(module, file, sdkPath, sdkName) {
348
+ logger.trace('=== parse sdk file:', file);
349
+ try {
350
+ const arkFile = new ArkFile_1.ArkFile(FileUtils_1.FileUtils.getFileLanguage(file, this.scene.getFileLanguages()));
351
+ arkFile.setScene(this.scene);
352
+ ArktsFrontend_1.ArktsFrontend.buildArkFileFromSdkPath(file, sdkPath, arkFile, sdkName);
353
+ ModelUtils_1.ModelUtils.getAllClassesInFile(arkFile).forEach(cls => {
354
+ var _a, _b;
355
+ (_a = cls.getDefaultArkMethod()) === null || _a === void 0 ? void 0 : _a.buildBody();
356
+ (_b = cls.getDefaultArkMethod()) === null || _b === void 0 ? void 0 : _b.freeBodyBuilder();
357
+ });
358
+ // Release all method builders in SDK file to avoid retaining AST/build context in memory.
359
+ ModelUtils_1.ModelUtils.getAllMethodsInFile(arkFile).forEach(method => {
360
+ var _a;
361
+ if (((_a = method.getDeclaringArkFile()) === null || _a === void 0 ? void 0 : _a.getLanguage()) === ArkFile_1.Language.CXX) {
362
+ method.freeCxxBodyBuilder();
363
+ }
364
+ else {
365
+ method.freeBodyBuilder();
366
+ }
367
+ });
368
+ module.addFile(arkFile);
369
+ this.scene.addSdkArkFile(arkFile);
370
+ SdkUtils_1.SdkUtils.buildSdkImportMap(arkFile);
371
+ SdkUtils_1.SdkUtils.loadGlobalAPI(arkFile, this.scene.getSdkGlobalMap());
372
+ }
373
+ catch (error) {
374
+ logger.error('Error parsing file:', file, error);
375
+ this.scene.getUnhandledSdkFilePaths().push(file);
376
+ }
377
+ }
378
+ /**
379
+ * Read `build-profile.json5` from the project root and call
380
+ * {@link SdkUtils.setEsVersion} so that {@link SdkUtils.fetchBuiltInFiles} selects the correct
381
+ * `lib.*.d.ts` entry.
382
+ */
383
+ setEsVersionFromBuildProfile() {
384
+ const buildProfilePath = path_1.default.join(this.scene.getRealProjectDir(), EtsConst_1.BUILD_PROFILE_JSON5);
385
+ if (!fs_1.default.existsSync(buildProfilePath)) {
386
+ return;
387
+ }
388
+ let configurationsText;
389
+ try {
390
+ configurationsText = fs_1.default.readFileSync(buildProfilePath, 'utf-8');
391
+ }
392
+ catch (error) {
393
+ logger.error(`Error reading build-profile.json5: ${error}`);
394
+ return;
395
+ }
396
+ const buildProfileJson = (0, json5parser_1.parseJsonText)(configurationsText);
397
+ SdkUtils_1.SdkUtils.setEsVersion(buildProfileJson);
398
+ }
399
+ /**
400
+ * Prepare modules by discovering and registering project modules and oh_modules dependencies.
401
+ *
402
+ * Reads build-profile.json5 to discover project modules, then scans oh_modules directories
403
+ * to discover third-party dependencies. Only basic info (paths) is registered —
404
+ * oh-package.json5 is NOT read and no dependency graph is built.
405
+ *
406
+ * Idempotent: if module preparation has already been completed via
407
+ * `Scene.setModulesRegistered(true)`, this method returns immediately.
408
+ * Note: this method does NOT set modulesRegistered — that is done by
409
+ * `Scene.analyseByModule` after both prepareModules and analyzeModuleDependencies complete.
410
+ */
411
+ prepareModules() {
412
+ if (this.scene.isModulesRegistered()) {
413
+ return;
414
+ }
415
+ this.registerProjectModules();
416
+ this.registerOhModulesModules();
417
+ }
418
+ /**
419
+ * Read build-profile.json5 from the project root and register each module as
420
+ * ArkModule(moduleType=PROJECT). The module's srcPath is resolved to an absolute path
421
+ * via path.resolve(projectDir, srcPath). oh-package.json5 is NOT read.
422
+ */
423
+ registerProjectModules() {
424
+ const projectDir = this.scene.getRealProjectDir();
425
+ const buildProfilePath = path_1.default.join(projectDir, EtsConst_1.BUILD_PROFILE_JSON5);
426
+ if (!fs_1.default.existsSync(buildProfilePath)) {
427
+ logger.warn('There is no build-profile.json5 for this project.');
428
+ return;
429
+ }
430
+ let configurationsText;
431
+ try {
432
+ configurationsText = fs_1.default.readFileSync(buildProfilePath, 'utf-8');
433
+ }
434
+ catch (error) {
435
+ logger.error(`Error reading build-profile.json5: ${error}`);
436
+ return;
437
+ }
438
+ const buildProfileJson = (0, json5parser_1.parseJsonText)(configurationsText);
439
+ const modules = buildProfileJson.modules;
440
+ if (modules instanceof Array) {
441
+ for (const module of modules) {
442
+ const modulePath = path_1.default.resolve(projectDir, module.srcPath);
443
+ this.registerModule(modulePath, module.name);
444
+ }
445
+ }
446
+ }
447
+ /**
448
+ * Scan oh_modules directories and register third-party dependencies as
449
+ * ArkModule(moduleType=OH_MODULES).
450
+ *
451
+ * Scans two types of oh_modules directories:
452
+ * 1. The project-level oh_modules/ under the project root.
453
+ * 2. The oh_modules/ under each registered PROJECT module's directory.
454
+ *
455
+ * A Set is used to avoid scanning the same directory twice.
456
+ */
457
+ registerOhModulesModules() {
458
+ const projectDir = this.scene.getRealProjectDir();
459
+ const scannedDirs = new Set();
460
+ // 1. Scan project-level oh_modules/
461
+ const projectOhModulesDir = path_1.default.join(projectDir, EtsConst_1.OH_MODULES);
462
+ if (fs_1.default.existsSync(projectOhModulesDir)) {
463
+ scannedDirs.add(projectOhModulesDir);
464
+ this.scanOhModulesDirectory(projectOhModulesDir);
465
+ }
466
+ // 2. Scan each PROJECT module's oh_modules/
467
+ for (const module of this.modulesIterator()) {
468
+ if (module.getModuleType() !== ArkModule_1.ModuleType.PROJECT) {
469
+ continue;
470
+ }
471
+ const moduleOhModulesDir = path_1.default.join(module.getModulePath(), EtsConst_1.OH_MODULES);
472
+ if (fs_1.default.existsSync(moduleOhModulesDir) && !scannedDirs.has(moduleOhModulesDir)) {
473
+ scannedDirs.add(moduleOhModulesDir);
474
+ this.scanOhModulesDirectory(moduleOhModulesDir);
475
+ }
476
+ }
477
+ }
478
+ /**
479
+ * Recursively scan a single oh_modules directory, registering discovered packages as
480
+ * ArkModule(moduleType=OH_MODULES).
481
+ *
482
+ * Directories starting with '@' (scoped packages) are recursed into.
483
+ * Regular directories have their symlinks resolved via fs.realpathSync() and are
484
+ * registered if not already present (deduplication by resolved real path).
485
+ *
486
+ * @param ohModulesDir - Absolute path of the oh_modules directory to scan.
487
+ */
488
+ scanOhModulesDirectory(ohModulesDir) {
489
+ let entries;
490
+ try {
491
+ entries = fs_1.default.readdirSync(ohModulesDir, { withFileTypes: true });
492
+ }
493
+ catch (error) {
494
+ logger.error(`Error reading oh_modules directory: ${ohModulesDir}`, error);
495
+ return;
496
+ }
497
+ for (const entry of entries) {
498
+ const entryPath = path_1.default.join(ohModulesDir, entry.name);
499
+ // Accept both real directories and symlinks (which may point to directories).
500
+ // Dirent.isDirectory() returns false for symlinks, so check isSymbolicLink() too.
501
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) {
502
+ continue;
503
+ }
504
+ if (entry.name.startsWith('@')) {
505
+ // Scoped package (e.g. @ohos/library): recurse into the scope directory
506
+ this.scanOhModulesDirectory(entryPath);
507
+ }
508
+ else {
509
+ // Regular package: resolve symlinks and register
510
+ const realPath = fs_1.default.realpathSync(entryPath);
511
+ if (this.getModuleByPath(realPath)) {
512
+ continue;
513
+ }
514
+ const module = this.registerModule(realPath, entry.name);
515
+ module.setModuleType(ArkModule_1.ModuleType.OH_MODULES);
516
+ }
517
+ }
518
+ }
519
+ // --- Dependency analysis ---
520
+ /**
521
+ * Main entry point for module dependency analysis.
522
+ *
523
+ * Reads oh-package.json5 to update module names, builds the dependency graph,
524
+ * runs SCC detection with post-processing refinement. The topological order and SCC groups
525
+ * are stored inside the dependency graph on the Scene, accessed via `ModuleBuilder.getTopoOrder()`.
526
+ *
527
+ * Idempotent: if the dependency graph already exists on the Scene, this method returns immediately.
528
+ */
529
+ analyzeModuleDependencies() {
530
+ if (this.scene.getModuleDepGraph()) {
531
+ return;
532
+ }
533
+ this.updateModuleNamesFromOhPkg();
534
+ this.buildDependencyGraph();
535
+ const graph = this.scene.getModuleDepGraph();
536
+ graph.refineSCCGroups(graph.getMaxSCCGroupSize());
537
+ }
538
+ /**
539
+ * Build the module dependency graph by reading each module's oh-package.json5.
540
+ *
541
+ * Creates a new {@link ModuleDepGraph} with the shared canonicalizer, adds all
542
+ * registered modules as nodes, then resolves dependencies and adds edges.
543
+ * Resolved dependencies create graph edges and update the module's alias map;
544
+ * unresolved dependencies are recorded as unresolved dependencies. The built graph is
545
+ * stored on the Scene via `Scene.setModuleDepGraph()`.
546
+ */
547
+ buildDependencyGraph() {
548
+ const graph = this.scene.createModuleDepGraph();
549
+ // 1. Add all registered modules as graph nodes
550
+ for (const module of this.modulesIterator()) {
551
+ graph.addModule(module);
552
+ }
553
+ // 2. Read project-level overrides and overrideDependencyMap from the project root oh-package.json5
554
+ const { overrides, overrideDependencyMap } = this.readProjectOverrides();
555
+ // 3. Process inter-module dependencies via oh-package.json5
556
+ for (const module of this.modulesIterator()) {
557
+ const deps = module.readOhPkgContent();
558
+ const depEntries = this.extractDependenciesWithValues(deps);
559
+ for (const [alias, depValue, depType] of depEntries) {
560
+ const depModule = this.resolveDepModule(alias, depValue, module.getModulePath(), overrides, overrideDependencyMap);
561
+ if (depModule) {
562
+ const srcId = this.scene.getModuleId(module);
563
+ const dstId = this.scene.getModuleId(depModule);
564
+ graph.addDependencyEdge(srcId, dstId, depType);
565
+ module.addDependencyAlias(alias, dstId);
566
+ }
567
+ else {
568
+ module.addUnresolvedDependency(alias, depValue);
569
+ }
570
+ }
571
+ }
572
+ this.scene.setModuleDepGraph(graph);
573
+ }
574
+ /**
575
+ * Read the project root oh-package.json5 and extract `overrides` and `overrideDependencyMap`.
576
+ *
577
+ * Both fields are optional dependency override configurations. `overrides` maps a dependency
578
+ * alias to an override path (local path, file: path, or @module: reference).
579
+ * `overrideDependencyMap` maps a dependency alias to an override file path. When a dependency
580
+ * alias is present in either map, the override path is used instead of the original dependency
581
+ * value during resolution (checked before normal resolution).
582
+ *
583
+ * @returns An object with `overrides` and `overrideDependencyMap` string maps (empty when absent).
584
+ */
585
+ readProjectOverrides() {
586
+ const empty = {};
587
+ const projectDir = this.scene.getRealProjectDir();
588
+ const ohPkgPath = path_1.default.join(projectDir, EtsConst_1.OH_PACKAGE_JSON5);
589
+ if (!fs_1.default.existsSync(ohPkgPath)) {
590
+ return { overrides: empty, overrideDependencyMap: empty };
591
+ }
592
+ let ohPkgContent;
593
+ try {
594
+ ohPkgContent = (0, json5parser_1.fetchDependenciesFromFile)(ohPkgPath);
595
+ }
596
+ catch (error) {
597
+ logger.error(`Error reading project oh-package.json5: ${error}`);
598
+ return { overrides: empty, overrideDependencyMap: empty };
599
+ }
600
+ const overrides = this.toStringMap(ohPkgContent.overrides);
601
+ const overrideDependencyMap = this.toStringMap(ohPkgContent.overrideDependencyMap);
602
+ return { overrides, overrideDependencyMap };
603
+ }
604
+ /**
605
+ * Coerce a record value to a `{ [k: string]: string }` map, dropping non-string entries.
606
+ */
607
+ toStringMap(value) {
608
+ const result = {};
609
+ if (value instanceof Object) {
610
+ for (const [k, v] of Object.entries(value)) {
611
+ if (typeof v === 'string') {
612
+ result[k] = v;
613
+ }
614
+ }
615
+ }
616
+ return result;
617
+ }
618
+ /**
619
+ * Extract dependency entries from oh-package.json5 content.
620
+ *
621
+ * Traverses dependencies (→ DEPENDENCIES), devDependencies (→ DEV_DEPENDENCIES),
622
+ * and dynamicDependencies (→ DYNAMIC), returning a [alias, value, DependencyType]
623
+ * triple for each entry. The DependencyType is used during SCC post-processing
624
+ * to determine edge removal priority when splitting oversized groups.
625
+ */
626
+ extractDependenciesWithValues(ohPkgContent) {
627
+ const result = [];
628
+ const depKeyToType = [
629
+ [Const_1.OH_PKG_DEPENDENCIES, ModuleDepGraph_1.DependencyType.DEPENDENCIES],
630
+ [Const_1.OH_PKG_DEV_DEPENDENCIES, ModuleDepGraph_1.DependencyType.DEV_DEPENDENCIES],
631
+ [Const_1.OH_PKG_DYNAMIC_DEPENDENCIES, ModuleDepGraph_1.DependencyType.DYNAMIC],
632
+ ];
633
+ for (const [key, depType] of depKeyToType) {
634
+ const deps = ohPkgContent[key];
635
+ if (deps instanceof Object) {
636
+ for (const [k, v] of Object.entries(deps)) {
637
+ if (typeof v === 'string') {
638
+ result.push([k, v, depType]);
639
+ }
640
+ }
641
+ }
642
+ }
643
+ return result;
644
+ }
645
+ /**
646
+ * Resolve a dependency declaration to its target {@link ArkModule}.
647
+ *
648
+ * Resolution priority:
649
+ * 0. overrides / overrideDependencyMap — when the alias is present in either project-level
650
+ * override map, the override path replaces the original dependency value before resolution.
651
+ * 1. {@link MODULE_PREFIX} prefix — find by moduleName among registered modules
652
+ * 2. "./", "../", or "file:" prefix — resolve as local path relative to
653
+ * scopeModulePath, look up in pathToId
654
+ * 3. Version number — look in oh_modules directories in priority order:
655
+ * a. current module's oh_modules (scopeModulePath/oh_modules/alias)
656
+ * b. project-level oh_modules (projectDir/oh_modules/alias)
657
+ * c. .ohpm cache (projectDir/oh_modules/.ohpm/<name>@<version>/oh_modules/<name>)
658
+ * Each candidate is verified as a directory, resolved via fs.realpathSync(), then looked
659
+ * up in pathToId. For .har dependencies inside the .ohpm cache, the current module's
660
+ * oh_modules base is recomputed from the enclosing oh_modules ancestor.
661
+ *
662
+ * @param alias - The dependency alias (key in oh-package.json5 dependencies).
663
+ * @param depValue - The dependency value (path, version, or @module: reference).
664
+ * @param scopeModulePath - The absolute path of the source module (for relative path resolution).
665
+ * @param overrides - Optional project-level overrides map (alias -> override path).
666
+ * @param overrideDependencyMap - Optional project-level overrideDependencyMap (alias -> override path).
667
+ * @returns The target ArkModule if resolved, undefined otherwise.
668
+ */
669
+ resolveDepModule(alias, depValue, scopeModulePath, overrides, overrideDependencyMap) {
670
+ // 0. Override handling: check overrides and overrideDependencyMap before normal resolution
671
+ if (overrides && overrides[alias] !== undefined) {
672
+ depValue = overrides[alias];
673
+ }
674
+ else if (overrideDependencyMap && overrideDependencyMap[alias] !== undefined) {
675
+ depValue = overrideDependencyMap[alias];
676
+ }
677
+ // 1. Module reference dependency: "@module:Foo"
678
+ if (depValue.startsWith(EtsConst_1.MODULE_PREFIX)) {
679
+ const refName = depValue.slice(EtsConst_1.MODULE_PREFIX.length);
680
+ for (const mod of this.modulesIterator()) {
681
+ if (mod.getModuleName() === refName) {
682
+ return mod;
683
+ }
684
+ }
685
+ return undefined;
686
+ }
687
+ // 2. Local path dependency: "./...", "../...", "file:..."
688
+ if (depValue.startsWith('./') || depValue.startsWith('../') || depValue.startsWith('file:')) {
689
+ const pathPart = depValue.startsWith('file:') ? depValue.slice('file:'.length) : depValue;
690
+ const resolvedPath = path_1.default.resolve(scopeModulePath, pathPart);
691
+ const id = this.pathToId.get(resolvedPath);
692
+ if (id !== undefined) {
693
+ return this.scene.getModule(id);
694
+ }
695
+ return undefined;
696
+ }
697
+ // 3. oh_modules dependency: version number like "^1.0.0"
698
+ const projectDir = this.scene.getRealProjectDir();
699
+ // For .har dependencies inside the .ohpm cache, recompute the oh_modules base from the
700
+ // enclosing oh_modules ancestor (mirrors processDependency in ModuleUtils).
701
+ const moduleBase = depValue.endsWith('.har') && scopeModulePath.includes(EtsConst_1.OHPM)
702
+ ? scopeModulePath.substring(0, scopeModulePath.lastIndexOf(EtsConst_1.OH_MODULES))
703
+ : scopeModulePath;
704
+ const candidates = [
705
+ path_1.default.resolve(moduleBase, EtsConst_1.OH_MODULES, alias),
706
+ path_1.default.resolve(projectDir, EtsConst_1.OH_MODULES, alias),
707
+ this.findModulePathInOHPM(projectDir, alias, depValue),
708
+ ];
709
+ for (const candidate of candidates) {
710
+ if (!candidate || !FileUtils_1.FileUtils.isDirectory(candidate)) {
711
+ continue;
712
+ }
713
+ const realPath = fs_1.default.realpathSync(candidate);
714
+ const id = this.pathToId.get(realPath);
715
+ if (id !== undefined) {
716
+ return this.scene.getModule(id);
717
+ }
718
+ }
719
+ return undefined;
720
+ }
721
+ /**
722
+ * Find a module path in the .ohpm cache directory.
723
+ *
724
+ * Mirrors {@link ModuleUtils.findModulePathInOHPM}: the cache lives at
725
+ * `<projectDir>/oh_modules/.ohpm` and stores packages as
726
+ * `<moduleName.replace('/', '+')>@<version>/oh_modules/<moduleName>`. Exact versions are
727
+ * matched directly; `^`-prefixed ranges pick the first directory whose version is not less
728
+ * than the requested minimum.
729
+ *
730
+ * @param projectDir - The project root directory.
731
+ * @param moduleName - The dependency alias / module name.
732
+ * @param version - The dependency version value (exact or `^`-prefixed).
733
+ * @returns The candidate directory path, or '' when not found.
734
+ */
735
+ findModulePathInOHPM(projectDir, moduleName, version) {
736
+ const ohpmPath = path_1.default.resolve(projectDir, EtsConst_1.OH_MODULES, EtsConst_1.OHPM);
737
+ const prefix = `${moduleName.replace('/', '+')}@`;
738
+ // exact version match
739
+ if (/^\d+(?:\.\d+){2}(?:-[0-9A-Za-z-]+)?$/.test(version)) {
740
+ return path_1.default.resolve(ohpmPath, prefix + version, EtsConst_1.OH_MODULES, moduleName);
741
+ }
742
+ // try to find the best version
743
+ try {
744
+ const dirs = fs_1.default.readdirSync(ohpmPath, { withFileTypes: true }).filter(dir => dir.isDirectory() && dir.name.startsWith(prefix));
745
+ for (const dir of dirs) {
746
+ const dirName = dir.name;
747
+ if (version.startsWith('^') && dirName.split('@')[1] < version.substring(1)) {
748
+ continue;
749
+ }
750
+ return path_1.default.resolve(ohpmPath, dirName, EtsConst_1.OH_MODULES, moduleName);
751
+ }
752
+ }
753
+ catch (e) {
754
+ logger.warn(`Cannot find module: ${moduleName} in ${ohpmPath}`);
755
+ }
756
+ return '';
757
+ }
758
+ /**
759
+ * Read oh-package.json5 for each registered module and update moduleName.
760
+ *
761
+ * The "name" field in oh-package.json5 (e.g. "@ohos/entry") is the canonical
762
+ * module name used for "@module:" reference resolution. This method populates
763
+ * {@link ArkModule.moduleName} from oh-package.json5, overriding the short name
764
+ * from build-profile.json5 set during registration.
765
+ */
766
+ updateModuleNamesFromOhPkg() {
767
+ for (const module of this.modulesIterator()) {
768
+ const ohPkgContent = module.readOhPkgContent();
769
+ const name = ohPkgContent.name;
770
+ if (typeof name !== 'string') {
771
+ continue;
772
+ }
773
+ module.setModuleName(name);
774
+ this.registerModulePath(module, name, ohPkgContent);
775
+ }
776
+ }
777
+ registerModulePath(module, name, ohPkgContent) {
778
+ if (ModuleUtils_1.ModuleUtils.MODULES.has(name)) {
779
+ return;
780
+ }
781
+ const modulePath = module.getModulePath();
782
+ let entry = ohPkgContent.types || ohPkgContent.main || '';
783
+ if (entry.endsWith('.js')) {
784
+ entry = entry.substring(0, entry.length - '.js'.length);
785
+ }
786
+ const mainPath = entry ? path_1.default.resolve(modulePath, entry) : modulePath;
787
+ ModuleUtils_1.ModuleUtils.MODULES.set(name, new ModuleUtils_1.ModulePath(modulePath, mainPath));
788
+ }
789
+ // --- Module loading ---
790
+ /**
791
+ * Map a {@link ModuleDepthLevel} to the corresponding {@link ModuleLoadState}.
792
+ * Used by {@link loadModule} to translate the configured depth level into a load state.
793
+ */
794
+ depthLevelToLoadState(level) {
795
+ switch (level) {
796
+ case ModuleDepth_1.ModuleDepthLevel.META:
797
+ return ArkModule_1.ModuleLoadState.META;
798
+ case ModuleDepth_1.ModuleDepthLevel.IMPORTS:
799
+ return ArkModule_1.ModuleLoadState.IMPORTS;
800
+ case ModuleDepth_1.ModuleDepthLevel.SIGNATURES:
801
+ return ArkModule_1.ModuleLoadState.SIGNATURES;
802
+ case ModuleDepth_1.ModuleDepthLevel.BODIES:
803
+ return ArkModule_1.ModuleLoadState.BODIES;
804
+ default:
805
+ return ArkModule_1.ModuleLoadState.META;
806
+ }
807
+ }
808
+ /**
809
+ * Reverse mapping of {@link depthLevelToLoadState}. Used to look up the heapUsed table
810
+ * entry for a module's current load state.
811
+ */
812
+ loadStateToDepthLevel(loadState) {
813
+ switch (loadState) {
814
+ case ArkModule_1.ModuleLoadState.META:
815
+ return ModuleDepth_1.ModuleDepthLevel.META;
816
+ case ArkModule_1.ModuleLoadState.IMPORTS:
817
+ return ModuleDepth_1.ModuleDepthLevel.IMPORTS;
818
+ case ArkModule_1.ModuleLoadState.SIGNATURES:
819
+ return ModuleDepth_1.ModuleDepthLevel.SIGNATURES;
820
+ case ArkModule_1.ModuleLoadState.BODIES:
821
+ return ModuleDepth_1.ModuleDepthLevel.BODIES;
822
+ default:
823
+ return ModuleDepth_1.ModuleDepthLevel.META;
824
+ }
825
+ }
826
+ /**
827
+ * Run type inference on the module's files, following the same pattern as
828
+ * {@link Scene.inferTypes} but scoped to a single module.
829
+ *
830
+ * Reuses the file topological order already computed by {@link analyzeFileDependencies}
831
+ * (stored in the module's {@link FileDepGraph}) so that depended-on files are inferred first.
832
+ * Falls back to unsorted iteration when no file dependency graph is available (e.g. when this
833
+ * method is called directly without a prior {@link loadModule}).
834
+ *
835
+ * At SIGNATURES level (no method bodies), only the `preInfer` phase is effective (generic
836
+ * types, parameter types, signature return types, import/export resolution). At BODIES level,
837
+ * full type inference runs (including stmt-level propagation and return type aggregation).
838
+ *
839
+ * Does NOT call {@link Scene.getMethodsMap}(true): rebuilding the global index would clear
840
+ * caches for other modules. Does NOT set {@link Scene.buildStage} to TYPE_INFERRED: that is a
841
+ * whole-scene flag. Does NOT call {@link SdkUtils.dispose} / {@link ModuleUtils.dispose} /
842
+ * {@link ValueUtil.dispose}: the global caches are shared across modules and should be
843
+ * released by the caller after all modules are processed.
844
+ *
845
+ * @param module - The module whose files are to be type-inferred.
846
+ * @param times - Number of inference iterations (clamped to 1–5). Default 1.
847
+ */
848
+ inferModuleTypes(module, times = 1) {
849
+ if (times < 1) {
850
+ return;
851
+ }
852
+ if (times > 5) {
853
+ times = 5;
854
+ }
855
+ // Reuse the file topological order computed by analyzeFileDependencies
856
+ const fileDepGraph = module.getFileDepGraph();
857
+ let sortedFiles;
858
+ if (fileDepGraph && fileDepGraph.getTopoOrder().length > 0) {
859
+ sortedFiles = fileDepGraph
860
+ .getTopoOrder()
861
+ .map(id => fileDepGraph.tryGetNode(id))
862
+ .filter((f) => f !== undefined);
863
+ }
864
+ else {
865
+ // Fallback: no file dep graph available (e.g. direct call without loadModule)
866
+ sortedFiles = Array.from(module.getFilesMap().values());
867
+ }
868
+ while (times > 0) {
869
+ for (const file of sortedFiles) {
870
+ Inference_1.InferenceManager.getInstance().getInference(file.getLanguage()).doInfer(file);
871
+ }
872
+ times--;
873
+ }
874
+ }
875
+ /**
876
+ * Load module data at the specified depth level.
877
+ *
878
+ * Dependencies are NOT recursively loaded here — the caller (e.g.
879
+ * {@link Scene.analyseByModule}) must iterate modules in topological order so that
880
+ * depended-on modules are loaded before their dependents.
881
+ *
882
+ * When cache management has been initialized via {@link initCacheManagement}, this method
883
+ * automatically checks memory before loading (evicting cached modules if over threshold)
884
+ * and registers the module in the cache after loading.
885
+ *
886
+ * Flow:
887
+ * 1. SDK modules are skipped — their file content is built by {@link buildSdkModules}, not here.
888
+ * 2. Idempotent: skip if the module's loadState already reaches the target load state.
889
+ * 3. Build module data to the effective level via {@link buildModuleToLevel}, which integrates
890
+ * intra-module file dependency analysis and topological-order parsing.
891
+ * 4. When the effective level reaches BODIES, build method bodies (ArkBody/CFG/Stmt/Expr) via
892
+ * {@link FrontendBuilder.buildModuleMethodBody}, following the same two-phase pattern as
893
+ * {@link Scene.genArkFiles}.
894
+ * 5. When the effective level reaches SIGNATURES, run type inference on the module's files
895
+ * via {@link inferModuleTypes}, referencing the logic of {@link Scene.inferTypes}.
896
+ * 6. Set the load state to the target level (after successful build).
897
+ *
898
+ * @param moduleId - ID of the module to load.
899
+ * @param depthLevel - The depth level to load the module to.
900
+ */
901
+ loadModule(moduleId, depthLevel = ModuleDepth_1.ModuleDepthLevel.META) {
902
+ var _a, _b, _c, _d, _e;
903
+ const module = this.getModule(moduleId);
904
+ if (!module) {
905
+ return;
906
+ }
907
+ // Cache management: evict cached modules if memory is over target
908
+ this.evictIfNeeded(moduleId, depthLevel);
909
+ // SDK modules: file content is built by buildSdkModules, not loadModule
910
+ if (module.getModuleType() === ArkModule_1.ModuleType.SDK) {
911
+ return;
912
+ }
913
+ const targetLoadState = this.depthLevelToLoadState(depthLevel);
914
+ // Idempotent: skip if already loaded to the target level, but ensure in cache
915
+ if (module.getLoadState() >= targetLoadState) {
916
+ (_a = this.cache) === null || _a === void 0 ? void 0 : _a.register(moduleId);
917
+ return;
918
+ }
919
+ // Record heapUsed before actual build (for heapUsed table population)
920
+ const recordHeapUsed = (_c = (_b = this.monitor) === null || _b === void 0 ? void 0 : _b.isEnabled()) !== null && _c !== void 0 ? _c : false;
921
+ const heapUsedBefore = recordHeapUsed ? this.monitor.getCurrentHeapUsed() : 0;
922
+ // Phase 1: build ArkFile objects to the effective level, with intra-module file
923
+ // dependency analysis integrated (topological-order parsing for level > IMPORTS).
924
+ this.buildModuleToLevel(module, depthLevel);
925
+ // Phase 2: build method bodies when BODIES level was requested
926
+ if (depthLevel >= ModuleDepth_1.ModuleDepthLevel.BODIES) {
927
+ FrontendBuilder_1.FrontendBuilder.buildModuleMethodBody(module);
928
+ }
929
+ // Phase 3: type inference when level >= SIGNATURES
930
+ if (depthLevel >= ModuleDepth_1.ModuleDepthLevel.SIGNATURES) {
931
+ this.inferModuleTypes(module);
932
+ }
933
+ // Set load state after successful build
934
+ module.setLoadState(targetLoadState);
935
+ // Record heapUsed increment in cache table (first load only — recordHeapUsed is a no-op if entry exists)
936
+ if (recordHeapUsed) {
937
+ const heapUsedAfter = this.monitor.getCurrentHeapUsed();
938
+ const heapUsedIncrement = Math.max(0, heapUsedAfter - heapUsedBefore);
939
+ (_d = this.cache) === null || _d === void 0 ? void 0 : _d.recordHeapUsed(moduleId, depthLevel, heapUsedIncrement);
940
+ }
941
+ // Register in cache after successful load
942
+ (_e = this.cache) === null || _e === void 0 ? void 0 : _e.register(moduleId);
943
+ }
944
+ /**
945
+ * Unload a module's data, releasing heavy data and resetting to NOT_LOADED state.
946
+ *
947
+ * Clears the module's filesMap (releasing ArkFile/ArkClass/ArkMethod objects), resets the
948
+ * file dependency graph (so that the next {@link loadModule} rebuilds IMPORTS from scratch),
949
+ * and sets the load state to NOT_LOADED. After unloading, {@link loadModule} can rebuild
950
+ * the module from scratch.
951
+ *
952
+ * SDK modules are not unloaded (their data is built by {@link buildSdkModules} and shared
953
+ * globally).
954
+ *
955
+ * @param moduleId - ID of the module to unload.
956
+ */
957
+ unload(moduleId) {
958
+ const module = this.getModule(moduleId);
959
+ if (!module) {
960
+ return;
961
+ }
962
+ if (module.getModuleType() === ArkModule_1.ModuleType.SDK) {
963
+ return;
964
+ }
965
+ this.scene.disposeModule(module);
966
+ this.breakInternalReferences(module);
967
+ module.clearFilesMap();
968
+ module.setFileDepGraph(undefined);
969
+ module.setLoadState(ArkModule_1.ModuleLoadState.NOT_LOADED);
970
+ }
971
+ /**
972
+ * Break all internal references between IR objects in a module's ArkFiles.
973
+ * Called after {@link Scene.disposeModule} to ensure that IR objects (ArkClass, ArkMethod,
974
+ * ArkBody, ArkField, ExportInfo, ImportInfo) do not retain each other through cross-references,
975
+ * allowing V8 GC to reclaim them individually.
976
+ */
977
+ breakInternalReferences(module) {
978
+ for (const arkFile of module.getFilesMap().values()) {
979
+ arkFile.clearAllReferences();
980
+ }
981
+ }
982
+ // --- Cache management ---
983
+ /**
984
+ * Initialize or update cache management for module loading.
985
+ *
986
+ * On the first call, creates a {@link ModuleCache} and {@link MemoryMonitor} and stores
987
+ * them persistently in {@link Scene}. On subsequent calls, reuses the persistent instances
988
+ * and only updates the per-call topological order.
989
+ *
990
+ * @param options - Scene options containing memoryLimitMB.
991
+ * @param topoOrder - Topological order of modules to be loaded (dependees before dependents).
992
+ * @param dependencyLoadLevel - Load level to downgrade protected dependencies to during eviction.
993
+ */
994
+ initCacheManagement(options, topoOrder, dependencyLoadLevel = ModuleDepth_1.ModuleDepthLevel.SIGNATURES) {
995
+ if (!this.scene.getModuleCache()) {
996
+ this.scene.setModuleCache(new ModuleCache_1.ModuleCache());
997
+ this.scene.setMemoryMonitor(new MemoryMonitor_1.MemoryMonitor(options));
998
+ }
999
+ this.monitor = this.scene.getMemoryMonitor();
1000
+ this.cache = this.scene.getModuleCache();
1001
+ this.topoOrder = topoOrder;
1002
+ this.dependencyLoadLevel = dependencyLoadLevel;
1003
+ this.topoOrderIndex = new Map();
1004
+ for (let i = 0; i < topoOrder.length; i++) {
1005
+ this.topoOrderIndex.set(topoOrder[i], i);
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Check memory and evict cached modules if needed, before loading a new module.
1010
+ *
1011
+ * Estimates the heapUsed increment of the upcoming load (from the heapUsed table if available,
1012
+ * otherwise from HEAPUSED_PER_FILE), computes a heapUsed upper limit (heapUsedLimit - estimated
1013
+ * increment), takes one actual heapUsed measurement, and only proceeds with eviction if
1014
+ * heapUsed exceeds the upper limit.
1015
+ *
1016
+ * During eviction, the heapUsed estimate is tracked via {@link HeapUsedEstimateState},
1017
+ * decremented by the heapUsed table value of each unloaded/downgraded module.
1018
+ */
1019
+ evictIfNeeded(moduleId, depthLevel = ModuleDepth_1.ModuleDepthLevel.META) {
1020
+ if (!this.monitor || !this.cache || !this.topoOrderIndex) {
1021
+ return;
1022
+ }
1023
+ if (!this.monitor.isEnabled()) {
1024
+ return;
1025
+ }
1026
+ const idx = this.topoOrderIndex.get(moduleId);
1027
+ if (idx === undefined) {
1028
+ return;
1029
+ }
1030
+ const module = this.getModule(moduleId);
1031
+ if (!module || module.getModuleType() === ArkModule_1.ModuleType.SDK) {
1032
+ return;
1033
+ }
1034
+ const estInc = this.estimateLoadHeapUsed(moduleId, module, depthLevel);
1035
+ const heapUsedUpperLimit = Math.max(0, this.monitor.getHeapUsedLimitBytes() - estInc);
1036
+ const heapUsedStart = this.monitor.getCurrentHeapUsed();
1037
+ if (heapUsedStart <= heapUsedUpperLimit) {
1038
+ return;
1039
+ }
1040
+ const heapUsedState = { estimate: heapUsedStart, limit: heapUsedUpperLimit };
1041
+ const depGraph = this.scene.getModuleDepGraph();
1042
+ const { hardProtected, softProtected } = this.computeProtectedSets(moduleId, idx, depGraph);
1043
+ const protectedDeps = new Set([...hardProtected, ...softProtected]);
1044
+ protectedDeps.delete(moduleId);
1045
+ const reverseTopo = [...this.topoOrder].reverse();
1046
+ this.runPhase1Eviction(hardProtected, softProtected, depGraph, heapUsedState);
1047
+ this.runPhase2Downgrade(protectedDeps, reverseTopo, heapUsedState);
1048
+ this.runPhase3Eviction(protectedDeps, reverseTopo, heapUsedState);
1049
+ }
1050
+ runPhase1Eviction(hardProtected, softProtected, depGraph, heapUsedState) {
1051
+ var _a, _b;
1052
+ (_a = this.cache) === null || _a === void 0 ? void 0 : _a.evict(hardProtected, softProtected, this.monitor, id => this.unload(id), (_b = depGraph === null || depGraph === void 0 ? void 0 : depGraph.getSCCGroups) === null || _b === void 0 ? void 0 : _b.call(depGraph), heapUsedState, id => this.estimateUnloadHeapUsedDecrease(id));
1053
+ }
1054
+ runPhase2Downgrade(protectedDeps, reverseTopo, heapUsedState) {
1055
+ var _a;
1056
+ if (heapUsedState.estimate <= heapUsedState.limit) {
1057
+ return;
1058
+ }
1059
+ const downgradeTarget = Math.max(this.dependencyLoadLevel, ModuleDepth_1.ModuleDepthLevel.SIGNATURES);
1060
+ const targetLoadState = this.depthLevelToLoadState(downgradeTarget);
1061
+ for (const id of reverseTopo) {
1062
+ if (heapUsedState.estimate <= heapUsedState.limit) {
1063
+ break;
1064
+ }
1065
+ if (!protectedDeps.has(id) || !((_a = this.cache) === null || _a === void 0 ? void 0 : _a.has(id))) {
1066
+ continue;
1067
+ }
1068
+ const mod = this.getModule(id);
1069
+ if (!mod || mod.getLoadState() <= targetLoadState) {
1070
+ continue;
1071
+ }
1072
+ const decrease = this.estimateDowngradeHeapUsedDecrease(id, downgradeTarget);
1073
+ this.downgradeModule(id, downgradeTarget);
1074
+ heapUsedState.estimate -= decrease;
1075
+ }
1076
+ }
1077
+ runPhase3Eviction(protectedDeps, reverseTopo, heapUsedState) {
1078
+ var _a;
1079
+ if (heapUsedState.estimate <= heapUsedState.limit) {
1080
+ return;
1081
+ }
1082
+ for (const id of reverseTopo) {
1083
+ if (heapUsedState.estimate <= heapUsedState.limit) {
1084
+ break;
1085
+ }
1086
+ if (protectedDeps.has(id) && ((_a = this.cache) === null || _a === void 0 ? void 0 : _a.has(id))) {
1087
+ const decrease = this.estimateUnloadHeapUsedDecrease(id);
1088
+ this.unload(id);
1089
+ this.cache.unregister(id);
1090
+ heapUsedState.estimate -= decrease;
1091
+ }
1092
+ }
1093
+ }
1094
+ /**
1095
+ * Estimate the memory cost of loading a module at the given depth level.
1096
+ * Used as a fallback when the heapUsed table has no entry for this module+level.
1097
+ */
1098
+ estimateLoadCost(module, depthLevel) {
1099
+ var _a;
1100
+ const fileCount = this.getFileCount(module);
1101
+ const bytesPerFile = (_a = ModuleBuilder.HEAPUSED_PER_FILE.get(depthLevel)) !== null && _a !== void 0 ? _a : 0;
1102
+ return fileCount * bytesPerFile;
1103
+ }
1104
+ /**
1105
+ * Estimate the heapUsed increment for loading a module at the given depth level.
1106
+ * Uses the heapUsed table value if available (from a prior first-load measurement),
1107
+ * otherwise falls back to {@link estimateLoadCost} (fileCount × HEAPUSED_PER_FILE).
1108
+ */
1109
+ estimateLoadHeapUsed(moduleId, module, depthLevel) {
1110
+ var _a;
1111
+ const tableHeapUsed = (_a = this.cache) === null || _a === void 0 ? void 0 : _a.getHeapUsed(moduleId, depthLevel);
1112
+ if (tableHeapUsed !== undefined) {
1113
+ return tableHeapUsed;
1114
+ }
1115
+ return this.estimateLoadCost(module, depthLevel);
1116
+ }
1117
+ /**
1118
+ * Estimate the heapUsed decrease from unloading a module (full unload).
1119
+ * Uses the heapUsed table value for the module's current load level,
1120
+ * or falls back to HEAPUSED_PER_FILE estimate.
1121
+ */
1122
+ estimateUnloadHeapUsedDecrease(moduleId) {
1123
+ var _a;
1124
+ const module = this.getModule(moduleId);
1125
+ if (!module) {
1126
+ return 0;
1127
+ }
1128
+ const currentLevel = this.loadStateToDepthLevel(module.getLoadState());
1129
+ const tableHeapUsed = (_a = this.cache) === null || _a === void 0 ? void 0 : _a.getHeapUsed(moduleId, currentLevel);
1130
+ if (tableHeapUsed !== undefined) {
1131
+ return tableHeapUsed;
1132
+ }
1133
+ return this.estimateLoadCost(module, currentLevel);
1134
+ }
1135
+ /**
1136
+ * Estimate the heapUsed decrease from downgrading a module from its current level
1137
+ * to a target level. Uses a ratio-based approach: downgrade releases a fixed
1138
+ * proportion of the current-level heapUsed, avoiding the need for target-level
1139
+ * table entries (which are never populated since downgradeModule doesn't write
1140
+ * to the heapUsed table).
1141
+ */
1142
+ estimateDowngradeHeapUsedDecrease(moduleId, _targetLevel) {
1143
+ var _a, _b;
1144
+ const module = this.getModule(moduleId);
1145
+ if (!module) {
1146
+ return 0;
1147
+ }
1148
+ const currentLevel = this.loadStateToDepthLevel(module.getLoadState());
1149
+ const currentHeapUsed = (_b = (_a = this.cache) === null || _a === void 0 ? void 0 : _a.getHeapUsed(moduleId, currentLevel)) !== null && _b !== void 0 ? _b : this.estimateLoadCost(module, currentLevel);
1150
+ return Math.max(0, Math.floor(currentHeapUsed * ModuleBuilder.DOWNGRADE_RELEASE_RATIO));
1151
+ }
1152
+ /**
1153
+ * Downgrade a module's loaded data to the given target level (min SIGNATURES), releasing the
1154
+ * heavy method-body/view-tree/AST data while keeping signatures (ArkClass/method signatures,
1155
+ * filesMap, Scene index maps) so dependents can still resolve types. No-op if already at or
1156
+ * below the target level. Used by eviction Phase 2.
1157
+ */
1158
+ downgradeModule(moduleId, targetLevel) {
1159
+ const module = this.getModule(moduleId);
1160
+ if (!module || module.getModuleType() === ArkModule_1.ModuleType.SDK) {
1161
+ return;
1162
+ }
1163
+ const targetLoadState = this.depthLevelToLoadState(targetLevel);
1164
+ if (module.getLoadState() <= targetLoadState) {
1165
+ return;
1166
+ }
1167
+ for (const arkFile of module.getFilesMap().values()) {
1168
+ for (const cls of ModelUtils_1.ModelUtils.getAllClassesInFile(arkFile)) {
1169
+ for (const mtd of cls.getMethods(true)) {
1170
+ mtd.clearBodyAndSupplementary();
1171
+ }
1172
+ cls.viewTree = undefined;
1173
+ for (const field of cls.getFields()) {
1174
+ field.setInitializer([]);
1175
+ }
1176
+ }
1177
+ arkFile.setAST(null);
1178
+ arkFile.clearSourceCode();
1179
+ }
1180
+ module.setLoadState(targetLoadState);
1181
+ }
1182
+ /**
1183
+ * Get the number of source files in a module. If the module's filesMap is empty
1184
+ * (not yet loaded), scans the module directory.
1185
+ */
1186
+ getFileCount(module) {
1187
+ var _a, _b;
1188
+ if (module.getFilesMap().size > 0) {
1189
+ return module.getFilesMap().size;
1190
+ }
1191
+ const options = this.scene.getOptions();
1192
+ const supportFileExts = (_a = options === null || options === void 0 ? void 0 : options.supportFileExts) !== null && _a !== void 0 ? _a : ['.ets', '.ts'];
1193
+ const ignoreFileNames = [...((_b = options === null || options === void 0 ? void 0 : options.ignoreFileNames) !== null && _b !== void 0 ? _b : []), EtsConst_1.OH_MODULES];
1194
+ return (0, getAllFiles_1.getAllFiles)(module.getModulePath(), supportFileExts, ignoreFileNames).length;
1195
+ }
1196
+ /**
1197
+ * Compute the hard and soft protected sets for eviction.
1198
+ * - Hard: the current module and its direct dependencies (never evicted).
1199
+ * - Soft: direct dependencies of not-yet-loaded modules, plus already-cached modules
1200
+ * at later topo positions (to avoid reload overhead).
1201
+ */
1202
+ computeProtectedSets(moduleId, idx, depGraph) {
1203
+ const hardProtected = new Set([moduleId]);
1204
+ const softProtected = new Set();
1205
+ if (!depGraph || !this.cache) {
1206
+ return { hardProtected, softProtected };
1207
+ }
1208
+ // Hard-protect current module's direct dependencies
1209
+ for (const depId of depGraph.getSuccModuleIds(moduleId)) {
1210
+ if (this.cache.has(depId)) {
1211
+ hardProtected.add(depId);
1212
+ }
1213
+ }
1214
+ // Soft-protect direct dependencies of not-yet-loaded modules
1215
+ // and already-cached modules at later positions (avoid reload)
1216
+ for (let j = idx + 1; j < this.topoOrder.length; j++) {
1217
+ for (const depId of depGraph.getSuccModuleIds(this.topoOrder[j])) {
1218
+ if (this.cache.has(depId)) {
1219
+ softProtected.add(depId);
1220
+ }
1221
+ }
1222
+ if (this.cache.has(this.topoOrder[j])) {
1223
+ softProtected.add(this.topoOrder[j]);
1224
+ }
1225
+ }
1226
+ return { hardProtected, softProtected };
1227
+ }
1228
+ /**
1229
+ * Build module data to the specified depth level, integrating intra-module file dependency
1230
+ * analysis into the build flow.
1231
+ *
1232
+ * Two-phase build for level > IMPORTS:
1233
+ * 1. Ensure ArkFile shells exist (create if filesMap is empty, reuse otherwise).
1234
+ * 2. Build all files to IMPORTS level (lightweight import/export parsing, no dependency on
1235
+ * other files in the module).
1236
+ * 3. Analyze intra-module file dependencies → compute topological order (moved here from
1237
+ * {@link loadModule} so it is part of the build-to-level flow).
1238
+ * 4. For level > IMPORTS: upgrade each file to the target level (SIGNATURES/BODIES) in
1239
+ * topological order, so that depended-on files are parsed first.
1240
+ *
1241
+ * For level == META: only shells are created (no content).
1242
+ * For level == IMPORTS: steps 1-3 (topoOrder available for later type inference).
1243
+ * For level >= SIGNATURES: steps 1-4 (signatures built in topoOrder).
1244
+ *
1245
+ * The {@link ArkModule.hasFileTopoOrder} flag is used to detect whether IMPORTS was already
1246
+ * built (e.g. when upgrading from IMPORTS to SIGNATURES), avoiding redundant re-parsing.
1247
+ *
1248
+ * @param module - The module to build.
1249
+ * @param level - The target depth level.
1250
+ */
1251
+ buildModuleToLevel(module, level) {
1252
+ // 1. Ensure ArkFile shells exist (reuse if already created, e.g. from a prior META load)
1253
+ let arkFiles;
1254
+ if (module.getFilesMap().size === 0) {
1255
+ arkFiles = FrontendBuilder_1.FrontendBuilder.createModuleFileShells(this.scene, module);
1256
+ }
1257
+ else {
1258
+ arkFiles = Array.from(module.getFilesMap().values());
1259
+ }
1260
+ // META: no content to build
1261
+ if (level <= ModuleDepth_1.ModuleDepthLevel.META) {
1262
+ return;
1263
+ }
1264
+ // 2. Build IMPORTS level (if not already done) + analyze file dependencies
1265
+ // Use fileDepGraph existence as indicator that IMPORTS was already built.
1266
+ if (level >= ModuleDepth_1.ModuleDepthLevel.IMPORTS && !module.hasFileTopoOrder()) {
1267
+ for (const arkFile of arkFiles) {
1268
+ try {
1269
+ FrontendBuilder_1.FrontendBuilder.buildImports(arkFile, arkFile.getLanguage());
1270
+ }
1271
+ catch (error) {
1272
+ logger.error(`Error building imports for ${arkFile.getFilePath()}: ${error}`);
1273
+ }
1274
+ }
1275
+ this.analyzeFileDependencies(module);
1276
+ }
1277
+ // 3. For level > IMPORTS: upgrade to target level in topological order
1278
+ if (level > ModuleDepth_1.ModuleDepthLevel.IMPORTS) {
1279
+ const fileDepGraph = module.getFileDepGraph();
1280
+ let sortedFiles;
1281
+ if (fileDepGraph && fileDepGraph.getTopoOrder().length > 0) {
1282
+ sortedFiles = fileDepGraph
1283
+ .getTopoOrder()
1284
+ .map(id => fileDepGraph.tryGetNode(id))
1285
+ .filter((f) => f !== undefined);
1286
+ }
1287
+ else {
1288
+ // Fallback: no file dep graph available (e.g. empty module or analyzeFileDependencies produced no edges)
1289
+ sortedFiles = arkFiles;
1290
+ }
1291
+ for (const arkFile of sortedFiles) {
1292
+ try {
1293
+ FrontendBuilder_1.FrontendBuilder.upgradeArkFileToLevel(this.scene, arkFile, arkFile.getLanguage(), level);
1294
+ }
1295
+ catch (error) {
1296
+ logger.error(`Error upgrading ArkFile ${arkFile.getFilePath()}: ${error}`);
1297
+ }
1298
+ }
1299
+ }
1300
+ }
1301
+ // --- Intra-module file dependency analysis ---
1302
+ /**
1303
+ * Analyze file-to-file dependencies within a module using the import/export `from` specifiers
1304
+ * populated at IMPORTS level.
1305
+ *
1306
+ * Builds a {@link FileDepGraph}, resolves relative `from` specifiers to file paths within the
1307
+ * same module, adds dependency edges, computes a topological order via SCC detection, and
1308
+ * stores the graph on the {@link ArkModule}. The topological order is retained inside the
1309
+ * FileDepGraph and queried via {@link ArkModule.hasFileTopoOrder}.
1310
+ *
1311
+ * - Only relative `from` specifiers (`./`, `../`) are resolved; bare specifiers are ignored.
1312
+ * - Resolution matches the specifier against already-existing ArkFile paths in the module's filesMap.
1313
+ * - Only files within the module (present in the module's filesMap) get edges; external files are ignored.
1314
+ *
1315
+ * @param module - The module whose files are analyzed.
1316
+ */
1317
+ analyzeFileDependencies(module) {
1318
+ const filesMap = module.getFilesMap();
1319
+ if (filesMap.size === 0) {
1320
+ return;
1321
+ }
1322
+ // Build a path-to-ArkFile lookup for this module
1323
+ const pathToFile = new Map();
1324
+ for (const arkFile of filesMap.values()) {
1325
+ pathToFile.set(arkFile.getFilePath(), arkFile);
1326
+ }
1327
+ // Create a canonicalizer for ArkFile <-> NodeID mapping
1328
+ const fileCanonicalizer = new Canonicalizer_1.Canonicalizer();
1329
+ const fileDepGraph = new FileDepGraph_1.FileDepGraph(fileCanonicalizer);
1330
+ // Add all files as graph nodes
1331
+ for (const arkFile of filesMap.values()) {
1332
+ fileDepGraph.addFile(arkFile);
1333
+ }
1334
+ // Process import/export from specifiers to add dependency edges
1335
+ for (const arkFile of filesMap.values()) {
1336
+ const srcId = fileCanonicalizer.getId(arkFile);
1337
+ const fromSpecifiers = this.collectFromSpecifiers(arkFile);
1338
+ for (const from of fromSpecifiers) {
1339
+ // Only resolve relative specifiers
1340
+ if (!from.startsWith('./') && !from.startsWith('../')) {
1341
+ continue;
1342
+ }
1343
+ const resolvedPath = this.resolveFromSpecifier(from, arkFile, pathToFile);
1344
+ if (!resolvedPath) {
1345
+ continue;
1346
+ }
1347
+ const dstFile = pathToFile.get(resolvedPath);
1348
+ if (!dstFile) {
1349
+ continue;
1350
+ }
1351
+ const dstId = fileCanonicalizer.getId(dstFile);
1352
+ fileDepGraph.addDependencyEdge(srcId, dstId);
1353
+ }
1354
+ }
1355
+ // Compute topological order (stored inside the FileDepGraph)
1356
+ fileDepGraph.computeTopoOrder();
1357
+ module.setFileDepGraph(fileDepGraph);
1358
+ }
1359
+ /**
1360
+ * Collect all `from` specifiers from an ArkFile's import and export infos.
1361
+ * Duplicate specifiers are deduplicated.
1362
+ */
1363
+ collectFromSpecifiers(arkFile) {
1364
+ const fromSet = new Set();
1365
+ for (const importInfo of arkFile.getImportInfos()) {
1366
+ const from = importInfo.getFrom();
1367
+ if (from) {
1368
+ fromSet.add(from);
1369
+ }
1370
+ }
1371
+ for (const exportInfo of arkFile.getExportInfos()) {
1372
+ const from = exportInfo.getFrom();
1373
+ if (from) {
1374
+ fromSet.add(from);
1375
+ }
1376
+ }
1377
+ return fromSet;
1378
+ }
1379
+ /**
1380
+ * Resolve a relative `from` specifier (e.g. `./b`, `../utils/helper`) to an absolute file path
1381
+ * that exists as an already-built ArkFile in the module.
1382
+ *
1383
+ * Resolution matches against the keys of {@link pathToFile} (the module's already-generated
1384
+ * ArkFile paths) instead of probing the filesystem:
1385
+ *
1386
+ * 1. The specifier as-is (may already include an extension).
1387
+ * 2. The specifier with each unique extension found among existing ArkFile paths appended.
1388
+ * 3. The specifier as a directory: look for an index file whose path is in {@link pathToFile}.
1389
+ *
1390
+ * @param from - The relative from specifier (starts with `./` or `../`).
1391
+ * @param arkFile - The file containing the import/export (base for relative resolution).
1392
+ * @param pathToFile - Map of already-built ArkFile paths in the module.
1393
+ * @returns The resolved absolute file path, or undefined if not found.
1394
+ */
1395
+ resolveFromSpecifier(from, arkFile, pathToFile) {
1396
+ const baseDir = path_1.default.dirname(arkFile.getFilePath());
1397
+ const resolvedPath = path_1.default.resolve(baseDir, from);
1398
+ // 1. Try as-is (specifier may already include an extension)
1399
+ if (pathToFile.has(resolvedPath)) {
1400
+ return resolvedPath;
1401
+ }
1402
+ // 2. Try appending extensions collected from existing files in the module
1403
+ const exts = new Set();
1404
+ for (const filePath of pathToFile.keys()) {
1405
+ const ext = path_1.default.extname(filePath);
1406
+ if (ext) {
1407
+ exts.add(ext);
1408
+ }
1409
+ }
1410
+ for (const ext of exts) {
1411
+ const candidate = resolvedPath + ext;
1412
+ if (pathToFile.has(candidate)) {
1413
+ return candidate;
1414
+ }
1415
+ }
1416
+ // 3. Try as a directory with an index file
1417
+ try {
1418
+ const indexFileName = FileUtils_1.FileUtils.getIndexFileName(resolvedPath);
1419
+ if (indexFileName) {
1420
+ const indexPath = path_1.default.join(resolvedPath, indexFileName);
1421
+ if (pathToFile.has(indexPath)) {
1422
+ return indexPath;
1423
+ }
1424
+ }
1425
+ }
1426
+ catch (_a) {
1427
+ // resolvedPath is not a readable directory; skip index file resolution
1428
+ }
1429
+ return undefined;
1430
+ }
1431
+ }
1432
+ exports.ModuleBuilder = ModuleBuilder;
1433
+ /**
1434
+ * Estimated heapUsed bytes per file for each depth level, used for pre-load memory cost estimation.
1435
+ * Calibrated from scene_board_ext measurements (no-eviction, no GC runs):
1436
+ * - BODIES: P75 of per-module heapUsed-per-file at BODIES level = ~1.7MB/file
1437
+ * - SIGNATURES: P75 of per-module heapUsed-per-file at SIGNATURES level = ~700KB/file
1438
+ * - IMPORTS/META: negligible (metadata only).
1439
+ *
1440
+ * No-GC calibration matches production conditions (no forced GC). Without GC,
1441
+ * heapUsed increments include uncollected garbage from parsing and type inference,
1442
+ * which is several times larger than surviving data.
1443
+ *
1444
+ * Used for pre-load estimation (deciding whether eviction is needed before loading)
1445
+ * and as a fallback for unload estimation when the heapUsed table has no entry.
1446
+ */
1447
+ ModuleBuilder.HEAPUSED_PER_FILE = new Map([
1448
+ [ModuleDepth_1.ModuleDepthLevel.META, 0],
1449
+ [ModuleDepth_1.ModuleDepthLevel.IMPORTS, 0],
1450
+ [ModuleDepth_1.ModuleDepthLevel.SIGNATURES, 700000],
1451
+ [ModuleDepth_1.ModuleDepthLevel.BODIES, 1700000],
1452
+ ]);
1453
+ /**
1454
+ * Ratio of memory released by downgrading from BODIES to SIGNATURES, relative to
1455
+ * the BODIES-level heapUsed. Calibrated from GC trace test data:
1456
+ * downgrade releases ~60% of BODIES heapUsed (method bodies + AST + source code).
1457
+ */
1458
+ ModuleBuilder.DOWNGRADE_RELEASE_RATIO = 0.6;