xray16 1.6.2 → 1.6.4

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.
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAliasedSymbol = resolveAliasedSymbol;
4
+ exports.getVirtualDeclaration = getVirtualDeclaration;
5
+ exports.isErasableReference = isErasableReference;
6
+ exports.resolveModuleSourceFile = resolveModuleSourceFile;
7
+ exports.isPureConstantsModule = isPureConstantsModule;
8
+ exports.validateVirtualModulePurity = validateVirtualModulePurity;
9
+ exports.collectVirtualDiagnostics = collectVirtualDiagnostics;
10
+ exports.getVirtualObjectEntries = getVirtualObjectEntries;
11
+ const ts = require("typescript");
12
+ const ast_1 = require("./ast");
13
+ const errors_1 = require("./errors");
14
+ const evaluation_1 = require("./evaluation");
15
+ const modulePurityCache = new WeakMap();
16
+ /**
17
+ * Resolve a symbol through import aliases.
18
+ *
19
+ * @param checker - Program type checker.
20
+ * @param symbol - Symbol to resolve.
21
+ * @returns Resolved symbol.
22
+ */
23
+ function resolveAliasedSymbol(checker, symbol) {
24
+ return (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
25
+ }
26
+ /**
27
+ * Get the `@virtual` declaration behind the provided symbol, if any.
28
+ *
29
+ * @param checker - Program type checker.
30
+ * @param symbol - Symbol to check.
31
+ * @returns Virtual tagged variable statement, enum declaration, or null.
32
+ */
33
+ function getVirtualDeclaration(checker, symbol) {
34
+ const declaration = resolveAliasedSymbol(checker, symbol).valueDeclaration;
35
+ if (declaration === undefined) {
36
+ return null;
37
+ }
38
+ if (ts.isEnumDeclaration(declaration)) {
39
+ return (0, ast_1.hasVirtualTag)(declaration) ? declaration : null;
40
+ }
41
+ if (ts.isVariableDeclaration(declaration)) {
42
+ const statement = (0, ast_1.getContainingVariableStatement)(declaration);
43
+ return statement !== null && (0, ast_1.hasVirtualTag)(statement) ? statement : null;
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * Check whether an access expression that references a tagged symbol is erasable.
49
+ * An access is erasable when the access itself inlines, a member access on it inlines,
50
+ * or it is an object spread that can be expanded.
51
+ *
52
+ * @param checker - Program type checker.
53
+ * @param access - Access expression referencing the symbol.
54
+ * @param symbol - Referenced tagged symbol.
55
+ * @param isVirtualSpreadAllowed - Whether object spread positions count as erasable.
56
+ * @returns Whether the access is erasable.
57
+ */
58
+ function isErasableAccess(checker, access, symbol, isVirtualSpreadAllowed) {
59
+ if ((0, evaluation_1.tryGetInlineValue)(checker, resolveAliasedSymbol(checker, symbol)) !== null) {
60
+ return true;
61
+ }
62
+ const parent = access.parent;
63
+ if (parent === undefined) {
64
+ return false;
65
+ }
66
+ if ((ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && parent.expression === access) {
67
+ const member = (0, evaluation_1.resolveMemberSymbol)(checker, parent);
68
+ return member !== null && (0, evaluation_1.tryGetInlineValue)(checker, member) !== null;
69
+ }
70
+ return ts.isSpreadAssignment(parent) && isVirtualSpreadAllowed;
71
+ }
72
+ /**
73
+ * Check whether an identifier reference to a tagged declaration is erasable.
74
+ * Erasable references are inlined, expanded, or stripped during transformation.
75
+ *
76
+ * @param checker - Program type checker.
77
+ * @param node - Identifier reference to check.
78
+ * @param symbol - Resolved symbol of the identifier.
79
+ * @param isVirtualSpreadAllowed - Whether object spread positions count as erasable.
80
+ * @returns Whether the reference is erasable.
81
+ */
82
+ function isErasableReference(checker, node, symbol, isVirtualSpreadAllowed) {
83
+ const parent = node.parent;
84
+ if (parent === undefined || ts.isPartOfTypeNode(node)) {
85
+ return true;
86
+ }
87
+ // In a type query such as 'typeof values', the entity name is not itself a type node:
88
+ if (ts.isTypeQueryNode(parent) || ts.isQualifiedName(parent)) {
89
+ return true;
90
+ }
91
+ // Own declaration names and import binding positions are stripped, not referenced:
92
+ if ((ts.isVariableDeclaration(parent) && parent.name === node) ||
93
+ (ts.isEnumDeclaration(parent) && parent.name === node) ||
94
+ ts.isImportSpecifier(parent) ||
95
+ ts.isImportClause(parent) ||
96
+ ts.isNamespaceImport(parent)) {
97
+ return true;
98
+ }
99
+ // In a namespace-qualified reference such as 'constants.value', the enclosing access is the real reference:
100
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
101
+ return isErasableAccess(checker, parent, symbol, isVirtualSpreadAllowed);
102
+ }
103
+ // Member access with build-time computable result gets inlined:
104
+ if ((ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && parent.expression === node) {
105
+ const member = (0, evaluation_1.resolveMemberSymbol)(checker, parent);
106
+ return member !== null && (0, evaluation_1.tryGetInlineValue)(checker, member) !== null;
107
+ }
108
+ // Object spreads of virtual objects are expanded to literal entries:
109
+ if (ts.isSpreadAssignment(parent)) {
110
+ return isVirtualSpreadAllowed;
111
+ }
112
+ // Scalar constants get inlined in plain value positions:
113
+ return (0, ast_1.isValueUsagePosition)(node) && (0, evaluation_1.tryGetInlineValue)(checker, resolveAliasedSymbol(checker, symbol)) !== null;
114
+ }
115
+ /**
116
+ * Check whether a statement is ambient (`declare`). Ambient statements produce no runtime output.
117
+ *
118
+ * @param statement - Statement to check.
119
+ * @returns Whether statement is ambient.
120
+ */
121
+ function isAmbientStatement(statement) {
122
+ return (ts.canHaveModifiers(statement) &&
123
+ (ts.getModifiers(statement)?.some((it) => it.kind === ts.SyntaxKind.DeclareKeyword) ?? false));
124
+ }
125
+ /**
126
+ * Check whether an import declaration binds only types, either through syntax or through resolved symbols.
127
+ *
128
+ * @param checker - Program type checker.
129
+ * @param statement - Import declaration to check.
130
+ * @returns Whether import produces no runtime bindings.
131
+ */
132
+ function isTypeOnlyImport(checker, statement) {
133
+ const clause = statement.importClause;
134
+ if (clause === undefined) {
135
+ return false;
136
+ }
137
+ if (clause.isTypeOnly) {
138
+ return true;
139
+ }
140
+ if (clause.name !== undefined || clause.namedBindings === undefined || !ts.isNamedImports(clause.namedBindings)) {
141
+ return false;
142
+ }
143
+ return clause.namedBindings.elements.every((specifier) => {
144
+ if (specifier.isTypeOnly) {
145
+ return true;
146
+ }
147
+ const symbol = checker.getSymbolAtLocation(specifier.name);
148
+ return symbol !== undefined && (resolveAliasedSymbol(checker, symbol).flags & ts.SymbolFlags.Value) === 0;
149
+ });
150
+ }
151
+ /**
152
+ * Check whether an import references an ambient module like 'xray16'.
153
+ * Ambient modules describe engine-provided globals, so importing them has no script side effects.
154
+ *
155
+ * @param checker - Program type checker.
156
+ * @param statement - Import declaration to check.
157
+ * @returns Whether the import references an ambient module.
158
+ */
159
+ function isAmbientModuleImport(checker, statement) {
160
+ const symbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
161
+ return symbol?.declarations?.some((it) => ts.isModuleDeclaration(it)) ?? false;
162
+ }
163
+ /**
164
+ * Resolve the source file for a module referenced by an import or export declaration specifier.
165
+ *
166
+ * @param checker - Program type checker.
167
+ * @param moduleSpecifier - Module specifier expression.
168
+ * @returns Resolved source file or null.
169
+ */
170
+ function resolveModuleSourceFile(checker, moduleSpecifier) {
171
+ const symbol = checker.getSymbolAtLocation(moduleSpecifier);
172
+ const declaration = symbol?.valueDeclaration;
173
+ return declaration !== undefined && ts.isSourceFile(declaration) ? declaration : null;
174
+ }
175
+ /**
176
+ * Check whether a module has no side effects on load. This silent analysis decides whether a require can be dropped.
177
+ * Pure modules contain only type-only imports, tagged constants, constant enums, types and
178
+ * imports or re-exports of other pure modules, including barrel files.
179
+ *
180
+ * @param checker - Program type checker.
181
+ * @param sourceFile - Module source file to check.
182
+ * @param seen - Modules on current resolution path, guards from circular imports.
183
+ * @returns Whether module load has no side effects.
184
+ */
185
+ function isPureConstantsModule(checker, sourceFile, seen = new Set()) {
186
+ const cached = modulePurityCache.get(sourceFile);
187
+ if (cached !== undefined) {
188
+ return cached;
189
+ }
190
+ if (seen.has(sourceFile)) {
191
+ return true;
192
+ }
193
+ seen.add(sourceFile);
194
+ const result = sourceFile.statements.every((statement) => {
195
+ if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement) || isAmbientStatement(statement)) {
196
+ return true;
197
+ }
198
+ if (ts.isVariableStatement(statement)) {
199
+ return (0, ast_1.hasInlineTag)(statement);
200
+ }
201
+ if (ts.isEnumDeclaration(statement)) {
202
+ return statement.members.every((member) => (0, evaluation_1.isComputableEnumMember)(checker, member));
203
+ }
204
+ if (ts.isImportDeclaration(statement)) {
205
+ if (isTypeOnlyImport(checker, statement) || isAmbientModuleImport(checker, statement)) {
206
+ return true;
207
+ }
208
+ const target = resolveModuleSourceFile(checker, statement.moduleSpecifier);
209
+ return target !== null && isPureConstantsModule(checker, target, seen);
210
+ }
211
+ if (ts.isExportDeclaration(statement)) {
212
+ if (statement.isTypeOnly) {
213
+ return true;
214
+ }
215
+ if (statement.moduleSpecifier === undefined) {
216
+ return true;
217
+ }
218
+ const target = resolveModuleSourceFile(checker, statement.moduleSpecifier);
219
+ return target !== null && isPureConstantsModule(checker, target, seen);
220
+ }
221
+ return false;
222
+ });
223
+ seen.delete(sourceFile);
224
+ modulePurityCache.set(sourceFile, result);
225
+ return result;
226
+ }
227
+ /**
228
+ * Validate strict purity for a module containing `@virtual` declarations.
229
+ * This is stricter than silent analysis: value imports and value re-exports are rejected even from pure modules,
230
+ * so erasing requires to this module can never silently drop transitive loads.
231
+ *
232
+ * @param checker - Program type checker.
233
+ * @param sourceFile - Module source file to validate.
234
+ * @returns List of produced diagnostics.
235
+ */
236
+ function validateVirtualModulePurity(checker, sourceFile) {
237
+ const diagnostics = [];
238
+ for (const statement of sourceFile.statements) {
239
+ const isAllowed = ts.isTypeAliasDeclaration(statement) ||
240
+ ts.isInterfaceDeclaration(statement) ||
241
+ isAmbientStatement(statement) ||
242
+ (ts.isVariableStatement(statement) && (0, ast_1.hasInlineTag)(statement)) ||
243
+ (ts.isEnumDeclaration(statement) &&
244
+ statement.members.every((member) => (0, evaluation_1.isComputableEnumMember)(checker, member))) ||
245
+ (ts.isImportDeclaration(statement) &&
246
+ (isTypeOnlyImport(checker, statement) || isAmbientModuleImport(checker, statement))) ||
247
+ (ts.isExportDeclaration(statement) && (statement.isTypeOnly || statement.moduleSpecifier === undefined));
248
+ if (!isAllowed) {
249
+ diagnostics.push((0, errors_1.createImpureVirtualModuleError)(statement));
250
+ }
251
+ }
252
+ return diagnostics;
253
+ }
254
+ /**
255
+ * Scan the program for references to `@virtual` declarations that cannot be erased, and for impure modules
256
+ * containing `@virtual` declarations.
257
+ *
258
+ * @param program - Program to scan.
259
+ * @returns List of produced diagnostics.
260
+ */
261
+ function collectVirtualDiagnostics(program) {
262
+ const checker = program.getTypeChecker();
263
+ const diagnostics = [];
264
+ const virtualNames = new Set();
265
+ const virtualModules = new Set();
266
+ // First pass: collect virtual declaration names for cheap identifier prefiltering.
267
+ for (const sourceFile of program.getSourceFiles()) {
268
+ if (sourceFile.isDeclarationFile) {
269
+ continue;
270
+ }
271
+ for (const statement of sourceFile.statements) {
272
+ if (!(0, ast_1.hasVirtualTag)(statement)) {
273
+ continue;
274
+ }
275
+ if (ts.isVariableStatement(statement)) {
276
+ for (const declaration of statement.declarationList.declarations) {
277
+ if (ts.isIdentifier(declaration.name)) {
278
+ virtualNames.add(declaration.name.text);
279
+ }
280
+ }
281
+ virtualModules.add(sourceFile);
282
+ }
283
+ else if (ts.isEnumDeclaration(statement)) {
284
+ virtualNames.add(statement.name.text);
285
+ virtualModules.add(sourceFile);
286
+ }
287
+ }
288
+ }
289
+ if (virtualNames.size === 0) {
290
+ return diagnostics;
291
+ }
292
+ for (const virtualModule of virtualModules) {
293
+ diagnostics.push(...validateVirtualModulePurity(checker, virtualModule));
294
+ }
295
+ // Second pass: every reference to a virtual declaration must be erasable.
296
+ for (const sourceFile of program.getSourceFiles()) {
297
+ if (sourceFile.isDeclarationFile) {
298
+ continue;
299
+ }
300
+ visitReferences(sourceFile);
301
+ }
302
+ return diagnostics;
303
+ function visitReferences(node) {
304
+ if (ts.isIdentifier(node) && virtualNames.has(node.text)) {
305
+ const symbol = checker.getSymbolAtLocation(node);
306
+ if (symbol !== undefined && getVirtualDeclaration(checker, symbol) !== null) {
307
+ if (ts.isExportSpecifier(node.parent)) {
308
+ diagnostics.push((0, errors_1.createVirtualValueReferenceError)(node, node.text));
309
+ }
310
+ else if (!isErasableReference(checker, node, symbol, true)) {
311
+ diagnostics.push((0, errors_1.createVirtualValueReferenceError)(node, node.text));
312
+ }
313
+ }
314
+ }
315
+ node.forEachChild(visitReferences);
316
+ }
317
+ }
318
+ /**
319
+ * Get entries of a virtual object declaration for spread expansion.
320
+ *
321
+ * @param checker - Program type checker.
322
+ * @param symbol - Virtual object symbol.
323
+ * @returns List of property name/value pairs, or null when an entry is not computable.
324
+ */
325
+ function getVirtualObjectEntries(checker, symbol) {
326
+ const declaration = resolveAliasedSymbol(checker, symbol).valueDeclaration;
327
+ if (declaration === undefined || !ts.isVariableDeclaration(declaration)) {
328
+ return null;
329
+ }
330
+ const entries = [];
331
+ const type = checker.getTypeAtLocation(declaration);
332
+ for (const property of type.getProperties()) {
333
+ const value = (0, evaluation_1.tryGetInlineValue)(checker, property);
334
+ if (value === null) {
335
+ return null;
336
+ }
337
+ entries.push([property.name, value]);
338
+ }
339
+ return entries;
340
+ }