Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Commit 77de611

Browse files
committed
Do not unquote declared properties, but emit a warning.
It turns out users do rely on manually quoted properties not to be renamed, even if there is a separate declaration for the property - at least we discovered one instance in Angular doing this.
1 parent 46c8afa commit 77de611

File tree

6 files changed

+46
-18
lines changed

6 files changed

+46
-18
lines changed

src/tsickle.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -764,9 +764,11 @@ class Annotator extends ClosureRewriter {
764764
}
765765
break;
766766
case ts.SyntaxKind.ElementAccessExpression:
767-
// Convert quoted accesses to properties that have a symbol to dotted accesses, to get
768-
// consistent Closure renaming. This can happen because TS allows quoted access with literal
769-
// strings to properties.
767+
// Warn for quoted accesses to properties that have a symbol declared.
768+
// Mixing quoted and non-quoted access to a symbol (x['foo'] and x.foo) risks breaking
769+
// Closure Compiler renaming. Quoted access is more cumbersome to write than dotted access
770+
// though, so chances are users did intend to avoid renaming. The better fix is to use
771+
// `declare interface` though.
770772
const eae = node as ts.ElementAccessExpression;
771773
if (!eae.argumentExpression ||
772774
eae.argumentExpression.kind !== ts.SyntaxKind.StringLiteral) {
@@ -776,11 +778,18 @@ class Annotator extends ClosureRewriter {
776778
// If it has a symbol, it's actually a regular declared property.
777779
if (!quotedPropSym) return false;
778780
const propName = (eae.argumentExpression as ts.StringLiteral).text;
779-
// Properties containing non-JS identifier names must not be unquoted.
781+
// Properties containing non-JS identifier names can only be accessed with quotes.
780782
if (!isValidClosurePropertyName(propName)) return false;
781-
this.writeNode(eae.expression);
782-
this.emit(`.${propName}`);
783-
return true;
783+
const symName = this.typeChecker.symbolToString(quotedPropSym);
784+
this.debugWarn(
785+
eae,
786+
`Declared property ${symName} accessed with quotes. ` +
787+
`This can lead to renaming bugs. A better fix is to use 'declare interface' ` +
788+
`on the declaration.`);
789+
// Previously, the code below changed the quoted into a non-quoted access.
790+
// this.writeNode(eae.expression);
791+
// this.emit(`.${propName}`);
792+
return false;
784793
case ts.SyntaxKind.PropertyAccessExpression:
785794
// Convert dotted accesses to types that have an index type declared to quoted accesses, to
786795
// avoid Closure renaming one access but not the other.

test_files/enum/enum.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ EnumTest1[EnumTest1.PI] = "PI";
1616
// number. Verify that the resulting TypeScript still allows you to
1717
// index into the enum with all the various ways allowed of enums.
1818
let /** @type {number} */ enumTestValue = EnumTest1.XYZ;
19-
let /** @type {number} */ enumTestValue2 = EnumTest1.XYZ;
19+
let /** @type {number} */ enumTestValue2 = EnumTest1['XYZ'];
2020
let /** @type {string} */ enumNumIndex = EnumTest1[((null))];
2121
let /** @type {number} */ enumStrIndex = EnumTest1[((null))];
2222
/**
@@ -25,7 +25,7 @@ let /** @type {number} */ enumStrIndex = EnumTest1[((null))];
2525
*/
2626
function enumTestFunction(val) { }
2727
enumTestFunction(enumTestValue);
28-
let /** @type {number} */ enumTestLookup = EnumTest1.XYZ;
28+
let /** @type {number} */ enumTestLookup = EnumTest1["XYZ"];
2929
let /** @type {?} */ enumTestLookup2 = EnumTest1["xyz".toUpperCase()];
3030
exports.EnumTest2 = {};
3131
/** @type {number} */

test_files/enum/enum.tsickle.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
Warning at test_files/enum/enum.ts:2:7: should not emit a 'never' type
2+
Warning at test_files/enum/enum.ts:9:33: Declared property XYZ accessed with quotes. This can lead to renaming bugs. A better fix is to use 'declare interface' on the declaration.
3+
Warning at test_files/enum/enum.ts:16:22: Declared property XYZ accessed with quotes. This can lead to renaming bugs. A better fix is to use 'declare interface' on the declaration.
24
====
35
/**
46
* @fileoverview added by tsickle
@@ -21,7 +23,7 @@ EnumTest1[EnumTest1.PI] = "PI";
2123
// number. Verify that the resulting TypeScript still allows you to
2224
// index into the enum with all the various ways allowed of enums.
2325
let /** @type {number} */ enumTestValue: EnumTest1 = EnumTest1.XYZ;
24-
let /** @type {number} */ enumTestValue2: EnumTest1 = EnumTest1.XYZ;
26+
let /** @type {number} */ enumTestValue2: EnumTest1 = EnumTest1['XYZ'];
2527
let /** @type {string} */ enumNumIndex: string = EnumTest1[ /** @type {number} */(( /** @type {?} */((null as any)) as number))];
2628
let /** @type {number} */ enumStrIndex: number = EnumTest1[ /** @type {string} */(( /** @type {?} */((null as any)) as string))];
2729
/**
@@ -31,7 +33,7 @@ let /** @type {number} */ enumStrIndex: number = EnumTest1[ /** @type {string} *
3133
function enumTestFunction(val: EnumTest1) {}
3234
enumTestFunction(enumTestValue);
3335

34-
let /** @type {number} */ enumTestLookup = EnumTest1.XYZ;
36+
let /** @type {number} */ enumTestLookup = EnumTest1["XYZ"];
3537
let /** @type {?} */ enumTestLookup2 = EnumTest1["xyz".toUpperCase()];
3638
export type EnumTest2 = number;
3739
export let EnumTest2: any = {};

test_files/quote_props/quote.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ QuotedMixed.prototype.foo;
2121
let /** @type {!QuotedMixed} */ quotedMixed = { foo: 1, 'invalid-identifier': 2 };
2222
console.log(quotedMixed.foo);
2323
quotedMixed.foo = 1;
24-
// Should be converted to non-quoted access.
25-
quotedMixed.foo = 1;
24+
// Intentionally kept as a quoted access, but gives a warning.
25+
quotedMixed['foo'] = 1;
2626
// Must not be converted to non-quoted access, as it's not valid JS.
27+
// Does not give a warning.
2728
quotedMixed['invalid-identifier'] = 1;
29+
// any does not declare any symbols.
30+
let /** @type {?} */ anyTyped;
31+
anyTyped['token'];

test_files/quote_props/quote.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ quoted.hello = 1;
1111
quoted['hello'] = 1;
1212

1313
interface QuotedMixed extends Quoted {
14-
// Assume that foo should be renamed, as it is explicitly declared.
14+
// Even though foo is explicitly declared as a property, assume it should not
15+
// be renamed.
1516
// It's unclear whether it's the right thing to do, user code might
1617
// access this field in a mixed fashion.
1718
foo: number;
@@ -21,7 +22,12 @@ let quotedMixed: QuotedMixed = {foo: 1, 'invalid-identifier': 2};
2122
console.log(quotedMixed.foo);
2223

2324
quotedMixed.foo = 1;
24-
// Should be converted to non-quoted access.
25+
// Intentionally kept as a quoted access, but gives a warning.
2526
quotedMixed['foo'] = 1;
2627
// Must not be converted to non-quoted access, as it's not valid JS.
28+
// Does not give a warning.
2729
quotedMixed['invalid-identifier'] = 1;
30+
31+
// any does not declare any symbols.
32+
let anyTyped: any;
33+
anyTyped['token'];

test_files/quote_props/quote.tsickle.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Warning at test_files/quote_props/quote.ts:9:13: Quoted has a string index type but is accessed using dotted access. Quoting the access.
22
Warning at test_files/quote_props/quote.ts:10:1: Quoted has a string index type but is accessed using dotted access. Quoting the access.
3+
Warning at test_files/quote_props/quote.ts:26:1: Declared property foo accessed with quotes. This can lead to renaming bugs. A better fix is to use 'declare interface' on the declaration.
34
====
45
/**
56
* @fileoverview added by tsickle
@@ -38,7 +39,8 @@ QuotedMixed.prototype.foo;
3839

3940

4041
interface QuotedMixed extends Quoted {
41-
// Assume that foo should be renamed, as it is explicitly declared.
42+
// Even though foo is explicitly declared as a property, assume it should not
43+
// be renamed.
4244
// It's unclear whether it's the right thing to do, user code might
4345
// access this field in a mixed fashion.
4446
foo: number;
@@ -48,7 +50,12 @@ let /** @type {!QuotedMixed} */ quotedMixed: QuotedMixed = {foo: 1, 'invalid-ide
4850
console.log(quotedMixed.foo);
4951

5052
quotedMixed.foo = 1;
51-
// Should be converted to non-quoted access.
52-
quotedMixed.foo = 1;
53+
// Intentionally kept as a quoted access, but gives a warning.
54+
quotedMixed['foo'] = 1;
5355
// Must not be converted to non-quoted access, as it's not valid JS.
56+
// Does not give a warning.
5457
quotedMixed['invalid-identifier'] = 1;
58+
59+
// any does not declare any symbols.
60+
let /** @type {?} */ anyTyped: any;
61+
anyTyped['token'];

0 commit comments

Comments
 (0)