ts-const-value-transformer 0.3.0 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.0
4
+
5
+ Add `unsafeHoistWritableValues` option to prevent from hoisting non-const values with literal type unexpectedly
6
+
3
7
  ## v0.3.0
4
8
 
5
9
  Add `externalNames` option for `hoistExternalValues`
package/README.md CHANGED
@@ -160,6 +160,8 @@ export interface TransformOptions {
160
160
  hoistPureFunctionCall?: boolean | undefined;
161
161
  /** Hoist expressions with `as XXX`. Default is false because the base (non-`as`) value may be non-constant. */
162
162
  unsafeHoistAsExpresion?: boolean | undefined;
163
+ /** Hoist properties/variables that can write (i.e. `let` / `var` variables or properies without `readonly`). Default is false because although the value is literal type at some point, the value may change to another literal type. */
164
+ unsafeHoistWritableValues?: boolean | undefined;
163
165
  /**
164
166
  * External names (tested with `.includes()` for string, with `.test()` for RegExp) for `hoistExternalValues` settings (If `hoistExternalValues` is not specified, this setting will be used).
165
167
  * - Path separators for input file name are always normalized to '/' internally.
@@ -253,6 +255,21 @@ The version string of this package.
253
255
 
254
256
  See [Transform options](#transform-options).
255
257
 
258
+ ## Notice
259
+
260
+ Starting from v0.4.0, `unsafeHoistWritableValues` option is introduced. Since TypeScript sometimes narrows non-constant values to literal types such as:
261
+
262
+ ```ts
263
+ const resultObject = { success: false };
264
+ someFunc1(resultObject);
265
+ console.log(resultObject.success); // resultObject.success will be `boolean` type
266
+ resultObject.success = false;
267
+ someFunc1(resultObject);
268
+ console.log(resultObject.success); // resultObject.success will be `false` type, not `boolean`
269
+ ```
270
+
271
+ ... so if `unsafeHoistWritableValues` is true, the second reference of `resultObject.success` above will be replaced to `false`, which may not be correct.
272
+
256
273
  ## Additional notes
257
274
 
258
275
  I think there should be more optimization methods by using strictly-typed information, like other programming languages.
@@ -15,6 +15,8 @@ export interface TransformOptions {
15
15
  hoistPureFunctionCall?: boolean | undefined;
16
16
  /** Hoist expressions with `as XXX`. Default is false because the base (non-`as`) value may be non-constant. */
17
17
  unsafeHoistAsExpresion?: boolean | undefined;
18
+ /** Hoist properties/variables that can write (i.e. `let` / `var` variables or properies without `readonly`). Default is false because although the value is literal type at some point, the value may change to another literal type. */
19
+ unsafeHoistWritableValues?: boolean | undefined;
18
20
  /**
19
21
  * External names (tested with `.includes()` for string, with `.test()` for RegExp) for `hoistExternalValues` settings (If `hoistExternalValues` is not specified, this setting will be used).
20
22
  * - Path separators for input file name are always normalized to '/' internally.
@@ -11,6 +11,7 @@ function assignDefaultValues(options = {}) {
11
11
  unsafeHoistAsExpresion: options.unsafeHoistAsExpresion ?? false,
12
12
  hoistPureFunctionCall: options.hoistPureFunctionCall ?? false,
13
13
  unsafeHoistFunctionCall: options.unsafeHoistFunctionCall ?? false,
14
+ unsafeHoistWritableValues: options.unsafeHoistWritableValues ?? false,
14
15
  externalNames: options.externalNames ?? [],
15
16
  };
16
17
  }
@@ -67,9 +68,7 @@ function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options
67
68
  (!ts.isExpression(node.parent) &&
68
69
  (!('initializer' in node.parent) ||
69
70
  node !== node.parent.initializer)) ||
70
- (!options.hoistProperty &&
71
- ts.isPropertyAccessExpression(node.parent) &&
72
- node === node.parent.name)) {
71
+ (ts.isPropertyAccessExpression(node.parent) && node === node.parent.name)) {
73
72
  return node;
74
73
  }
75
74
  }
@@ -78,6 +77,12 @@ function visitNodeAndReplaceIfNeeded(node, sourceFile, program, context, options
78
77
  hasParentAsExpression(node.parent, context, ts))) {
79
78
  return node;
80
79
  }
80
+ if (!options.unsafeHoistWritableValues) {
81
+ const r = isReadonlyExpression(node, program, ts);
82
+ if (r === false) {
83
+ return node;
84
+ }
85
+ }
81
86
  try {
82
87
  const typeChecker = program.getTypeChecker();
83
88
  const type = typeChecker.getTypeAtLocation(node);
@@ -233,6 +238,85 @@ function hasPureAnnotation(node, sourceFile, tsInstance) {
233
238
  }
234
239
  return false;
235
240
  }
241
+ function getMemberName(m, tsInstance) {
242
+ if (!m || !m.name) {
243
+ return '';
244
+ }
245
+ const name = m.name;
246
+ if (tsInstance.isIdentifier(name)) {
247
+ return name.escapedText;
248
+ }
249
+ else if (tsInstance.isPrivateIdentifier(name)) {
250
+ return name.escapedText;
251
+ }
252
+ else if (tsInstance.isStringLiteral(name)) {
253
+ return name.text;
254
+ }
255
+ else {
256
+ return '';
257
+ }
258
+ }
259
+ function isReadonlyPropertyAccess(a, typeChecker, tsInstance) {
260
+ const ts = tsInstance;
261
+ const type = typeChecker.getTypeAtLocation(a.expression);
262
+ const memberName = a.name.getText();
263
+ if (type.getFlags() & ts.TypeFlags.Object) {
264
+ const dummyTypeNode = typeChecker.typeToTypeNode(type, a, ts.NodeBuilderFlags.NoTruncation);
265
+ if (dummyTypeNode && ts.isTypeLiteralNode(dummyTypeNode)) {
266
+ for (let i = 0; i < dummyTypeNode.members.length; ++i) {
267
+ const m = dummyTypeNode.members[i];
268
+ if (m &&
269
+ getMemberName(m, ts) === memberName &&
270
+ ts.isPropertySignature(m)) {
271
+ if (m.modifiers?.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
272
+ return true;
273
+ }
274
+ }
275
+ }
276
+ }
277
+ const prop = type.getProperty(memberName);
278
+ if (prop && prop.declarations && prop.declarations.length > 0) {
279
+ const decl = prop.declarations[0];
280
+ if (ts.isPropertySignature(decl) &&
281
+ decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
282
+ return true;
283
+ }
284
+ if (ts.isVariableDeclaration(decl) &&
285
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
286
+ decl.parent &&
287
+ ts.isVariableDeclarationList(decl.parent) &&
288
+ decl.parent.flags & ts.NodeFlags.Const) {
289
+ return true;
290
+ }
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+ function isReadonlyExpression(node, program, tsInstance) {
296
+ const ts = tsInstance;
297
+ const typeChecker = program.getTypeChecker();
298
+ if (ts.isIdentifier(node) &&
299
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
300
+ node.parent &&
301
+ !ts.isPropertyAccessExpression(node.parent)) {
302
+ const nodeSym = typeChecker.getSymbolAtLocation(node);
303
+ if (nodeSym?.valueDeclaration) {
304
+ if (ts.isVariableDeclarationList(nodeSym.valueDeclaration.parent)) {
305
+ if (nodeSym.valueDeclaration.parent.flags & ts.NodeFlags.Const) {
306
+ return true;
307
+ }
308
+ return false;
309
+ }
310
+ }
311
+ }
312
+ if (ts.isPropertyAccessExpression(node)) {
313
+ if (isEnumAccess(node, program, ts)) {
314
+ return true;
315
+ }
316
+ return isReadonlyPropertyAccess(node, typeChecker, ts);
317
+ }
318
+ return null;
319
+ }
236
320
  ////////////////////////////////////////////////////////////////////////////////
237
321
  export function printSource(sourceFile) {
238
322
  return printSourceImpl(sourceFile)[0];
@@ -1,2 +1,2 @@
1
- declare const _default: "0.3.0";
1
+ declare const _default: "0.4.0";
2
2
  export default _default;
package/dist/version.mjs CHANGED
@@ -1 +1 @@
1
- export default '0.3.0';
1
+ export default '0.4.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-const-value-transformer",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "engines": {
5
5
  "node": ">=20.19.3"
6
6
  },