Skip to content

Commit

Permalink
Improve escape sequence handling in private names (#50856)
Browse files Browse the repository at this point in the history
* Add tests for identifiers and private identifiers with escape sequences.

* Accepted baselines.

* Store the tokenValue instead of tokenText on PrivateIdentifiers, since the latter can contain escapes and lead to semantic discrepancies.

* Accepted baselines.

* Check for leading escape sequences in PrivateIdentifiers.

* Accepted baselines.

* Fix lints.
  • Loading branch information
DanielRosenwasser committed Sep 20, 2022
1 parent 938a69a commit d90795e
Show file tree
Hide file tree
Showing 10 changed files with 1,761 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/compiler/parser.ts
Expand Up @@ -2281,7 +2281,7 @@ namespace ts {

function parsePrivateIdentifier(): PrivateIdentifier {
const pos = getNodePos();
const node = factory.createPrivateIdentifier(internPrivateIdentifier(scanner.getTokenText()));
const node = factory.createPrivateIdentifier(internPrivateIdentifier(scanner.getTokenValue()));
nextToken();
return finishNode(node, pos);
}
Expand Down
32 changes: 29 additions & 3 deletions src/compiler/scanner.ts
Expand Up @@ -2052,12 +2052,38 @@ namespace ts {
return token = SyntaxKind.Unknown;
}

if (isIdentifierStart(codePointAt(text, pos + 1), languageVersion)) {
const charAfterHash = codePointAt(text, pos + 1);
if (charAfterHash === CharacterCodes.backslash) {
pos++;
scanIdentifier(codePointAt(text, pos), languageVersion);
const extendedCookedChar = peekExtendedUnicodeEscape();
if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) {
pos += 3;
tokenFlags |= TokenFlags.ExtendedUnicodeEscape;
tokenValue = "#" + scanExtendedUnicodeEscape() + scanIdentifierParts();
return token = SyntaxKind.PrivateIdentifier;
}

const cookedChar = peekUnicodeEscape();
if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) {
pos += 6;
tokenFlags |= TokenFlags.UnicodeEscape;
tokenValue = "#" + String.fromCharCode(cookedChar) + scanIdentifierParts();
return token = SyntaxKind.PrivateIdentifier;
}
pos--;
}

if (isIdentifierStart(charAfterHash, languageVersion)) {
pos++;
// We're relying on scanIdentifier's behavior and adjusting the token kind after the fact.
// Notably absent from this block is the fact that calling a function named "scanIdentifier",
// but identifiers don't include '#', and that function doesn't deal with it at all.
// This works because 'scanIdentifier' tries to reuse source characters and builds up substrings;
// however, it starts at the 'tokenPos' which includes the '#', and will "accidentally" prepend the '#' for us.
scanIdentifier(charAfterHash, languageVersion);
}
else {
tokenValue = String.fromCharCode(codePointAt(text, pos));
tokenValue = "#";
error(Diagnostics.Invalid_character, pos++, charSize(ch));
}
return token = SyntaxKind.PrivateIdentifier;
Expand Down
@@ -0,0 +1,215 @@
//// [tests/cases/conformance/classes/members/privateNames/privateNamesEscapeSequences01.ts] ////

//// [IdentifierNameWithEscape1.ts]
export class IdentifierNameWithEscape1 {
\u0078: number;

constructor() {
this.\u0078 = 0;
}

doThing() {
this.x = 42;
}
}

//// [IdentifierNameWithEscape2.ts]
export class IdentifierNameWithEscape2 {
x\u0078: number;

constructor() {
this.x\u0078 = 0;
}

doThing() {
this.xx = 42;
}
}

//// [IdentifierNameWithExtendedEscape1.ts]
export class IdentifierNameWithExtendedEscape1 {
\u{78}: number;

constructor() {
this.\u{78} = 0;
}

doThing() {
this.x = 42;
}
}

//// [IdentifierNameWithExtendedEscape2.ts]
export class IdentifierNameWithExtendedEscape2 {
x\u{78}: number;

constructor() {
this.x\u{78} = 0;
}

doThing() {
this.xx = 42;
}
}

//// [PrivateIdentifierNameWithEscape1.ts]
export class PrivateIdentifierWithEscape1 {
#\u0078: number;

constructor() {
this.#\u0078 = 0;
}

doThing() {
this.#x = 42;
}
}

//// [PrivateIdentifierNameWithEscape2.ts]
export class PrivateIdentifierWithEscape2 {
#x\u0078: number;

constructor() {
this.#x\u0078 = 0;
}

doThing() {
this.#xx = 42;
}
}

//// [PrivateIdentifierNameWithExtendedEscape1.ts]
export class PrivateIdentifierWithExtendedEscape1 {
#\u{78}: number;

constructor() {
this.#\u{78} = 0;
}

doThing() {
this.#x = 42;
}
}

//// [PrivateIdentifierNameWithExtendedEscape2.ts]
export class PrivateIdentifierWithExtendedEscape2 {
#x\u{78}: number;

constructor() {
this.#x\u{78} = 0;
}

doThing() {
this.#xx = 42;
}
}


//// [IdentifierNameWithEscape1.js]
export class IdentifierNameWithEscape1 {
constructor() {
this.\u0078 = 0;
}
doThing() {
this.x = 42;
}
}
//// [IdentifierNameWithEscape2.js]
export class IdentifierNameWithEscape2 {
constructor() {
this.x\u0078 = 0;
}
doThing() {
this.xx = 42;
}
}
//// [IdentifierNameWithExtendedEscape1.js]
export class IdentifierNameWithExtendedEscape1 {
constructor() {
this.\u{78} = 0;
}
doThing() {
this.x = 42;
}
}
//// [IdentifierNameWithExtendedEscape2.js]
export class IdentifierNameWithExtendedEscape2 {
constructor() {
this.x\u{78} = 0;
}
doThing() {
this.xx = 42;
}
}
//// [PrivateIdentifierNameWithEscape1.js]
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _PrivateIdentifierWithEscape1_x;
export class PrivateIdentifierWithEscape1 {
constructor() {
_PrivateIdentifierWithEscape1_x.set(this, void 0);
__classPrivateFieldSet(this, _PrivateIdentifierWithEscape1_x, 0, "f");
}
doThing() {
__classPrivateFieldSet(this, _PrivateIdentifierWithEscape1_x, 42, "f");
}
}
_PrivateIdentifierWithEscape1_x = new WeakMap();
//// [PrivateIdentifierNameWithEscape2.js]
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _PrivateIdentifierWithEscape2_xx;
export class PrivateIdentifierWithEscape2 {
constructor() {
_PrivateIdentifierWithEscape2_xx.set(this, void 0);
__classPrivateFieldSet(this, _PrivateIdentifierWithEscape2_xx, 0, "f");
}
doThing() {
__classPrivateFieldSet(this, _PrivateIdentifierWithEscape2_xx, 42, "f");
}
}
_PrivateIdentifierWithEscape2_xx = new WeakMap();
//// [PrivateIdentifierNameWithExtendedEscape1.js]
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _PrivateIdentifierWithExtendedEscape1_x;
export class PrivateIdentifierWithExtendedEscape1 {
constructor() {
_PrivateIdentifierWithExtendedEscape1_x.set(this, void 0);
__classPrivateFieldSet(this, _PrivateIdentifierWithExtendedEscape1_x, 0, "f");
}
doThing() {
__classPrivateFieldSet(this, _PrivateIdentifierWithExtendedEscape1_x, 42, "f");
}
}
_PrivateIdentifierWithExtendedEscape1_x = new WeakMap();
//// [PrivateIdentifierNameWithExtendedEscape2.js]
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _PrivateIdentifierWithExtendedEscape2_xx;
export class PrivateIdentifierWithExtendedEscape2 {
constructor() {
_PrivateIdentifierWithExtendedEscape2_xx.set(this, void 0);
__classPrivateFieldSet(this, _PrivateIdentifierWithExtendedEscape2_xx, 0, "f");
}
doThing() {
__classPrivateFieldSet(this, _PrivateIdentifierWithExtendedEscape2_xx, 42, "f");
}
}
_PrivateIdentifierWithExtendedEscape2_xx = new WeakMap();

0 comments on commit d90795e

Please sign in to comment.