Skip to content

Commit 422bdb0

Browse files
committed
Fix unique symbol .d.ts generation and add baseline test
1 parent e635bb9 commit 422bdb0

File tree

8 files changed

+483
-14
lines changed

8 files changed

+483
-14
lines changed

src/compiler/emitter.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -423,15 +423,41 @@ import {
423423
WriteFileCallbackData,
424424
YieldExpression,
425425
} from "./_namespaces/ts.js";
426+
import { isTypeReferenceNode, TypeFlags, TypeChecker } from "./_namespaces/ts.js";
426427
import * as performance from "./_namespaces/ts.performance.js";
427-
428428
const brackets = createBracketsMap();
429429

430+
431+
430432
/** @internal */
433+
export function isUniqueSymbolDeclaration(node: VariableDeclarationList, checker: TypeChecker): boolean {
434+
return node.declarations.some((decl: VariableDeclaration) => {
435+
// 1. If type is explicitly written, handle as before
436+
const typeNode: TypeNode | undefined = decl.type;
437+
if (typeNode) {
438+
if (isTypeReferenceNode(typeNode) && isIdentifier(typeNode.typeName)) {
439+
if (typeNode.typeName.escapedText.toString() === "unique symbol") {
440+
return true;
441+
}
442+
}
443+
if ((typeNode.kind as SyntaxKind) === SyntaxKind.UniqueKeyword) {
444+
return true;
445+
}
446+
}
447+
// 2. Otherwise, check the inferred type
448+
const type = checker.getTypeAtLocation(decl.name);
449+
if (type.flags & TypeFlags.UniqueESSymbol) {
450+
return true;
451+
}
452+
453+
return false;
454+
});
455+
}
456+
431457
export function isBuildInfoFile(file: string): boolean {
432458
return fileExtensionIs(file, Extension.TsBuildInfo);
433459
}
434-
460+
435461
/**
436462
* Iterates over the source files that are expected to have an emit output.
437463
*
@@ -741,6 +767,7 @@ export function emitFiles(
741767
onlyBuildInfo: boolean,
742768
forceDtsEmit?: boolean,
743769
skipBuildInfo?: boolean,
770+
emitTypeChecker?: TypeChecker,
744771
): EmitResult {
745772
// Why var? It avoids TDZ checks in the runtime which can be costly.
746773
// See: https://github.com/microsoft/TypeScript/issues/52924
@@ -841,7 +868,7 @@ export function emitFiles(
841868
extendedDiagnostics: compilerOptions.extendedDiagnostics,
842869
};
843870

844-
// Create a printer to print the nodes
871+
const typeChecker = emitTypeChecker;
845872
const printer = createPrinter(printerOptions, {
846873
// resolver hooks
847874
hasGlobalName: resolver.hasGlobalName,
@@ -850,12 +877,11 @@ export function emitFiles(
850877
onEmitNode: transform.emitNodeWithNotification,
851878
isEmitNotificationEnabled: transform.isEmitNotificationEnabled,
852879
substituteNode: transform.substituteNode,
853-
});
880+
}, typeChecker);
854881

855882
Debug.assert(transform.transformed.length === 1, "Should only see one output from the transform");
856883
printSourceFileOrBundle(jsFilePath, sourceMapFilePath, transform, printer, compilerOptions);
857884

858-
// Clean up emit nodes on parse tree
859885
transform.dispose();
860886

861887
if (emittedFilesList) {
@@ -900,6 +926,8 @@ export function emitFiles(
900926
}
901927
}
902928

929+
// TypeChecker is already captured in the closure above
930+
903931
const declBlocked = (!!declarationTransform.diagnostics && !!declarationTransform.diagnostics.length) || !!host.isEmitBlocked(declarationFilePath) || !!compilerOptions.noEmit;
904932
emitSkipped = emitSkipped || declBlocked;
905933
if (!declBlocked || forceDtsEmit) {
@@ -1191,7 +1219,7 @@ export const createPrinterWithRemoveCommentsNeverAsciiEscape: () => Printer = /*
11911219
/** @internal */
11921220
export const createPrinterWithRemoveCommentsOmitTrailingSemicolon: () => Printer = /* @__PURE__ */ memoize(() => createPrinter({ removeComments: true, omitTrailingSemicolon: true }));
11931221

1194-
export function createPrinter(printerOptions: PrinterOptions = {}, handlers: PrintHandlers = {}): Printer {
1222+
export function createPrinter(printerOptions: PrinterOptions = {}, handlers: PrintHandlers = {}, typeChecker?: TypeChecker): Printer {
11951223
// Why var? It avoids TDZ checks in the runtime which can be costly.
11961224
// See: https://github.com/microsoft/TypeScript/issues/52924
11971225
/* eslint-disable no-var */
@@ -3343,7 +3371,6 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
33433371
writeSpace();
33443372
emit(node.caseBlock);
33453373
}
3346-
33473374
function emitLabeledStatement(node: LabeledStatement) {
33483375
emit(node.label);
33493376
emitTokenWithComment(SyntaxKind.ColonToken, node.label.end, writePunctuation, node);
@@ -3394,18 +3421,24 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
33943421
writeKeyword("await");
33953422
writeSpace();
33963423
writeKeyword("using");
3397-
}
3398-
else {
3399-
const head = isLet(node) ? "let" :
3424+
} else {
3425+
// Check if unique symbol and use const instead of var
3426+
const isUnique = typeChecker && isUniqueSymbolDeclaration(node, typeChecker);
3427+
const head =
3428+
isLet(node) ? "let" :
34003429
isVarConst(node) ? "const" :
34013430
isVarUsing(node) ? "using" :
3431+
isUnique ? "const" :
34023432
"var";
3433+
34033434
writeKeyword(head);
34043435
}
3405-
writeSpace();
3436+
3437+
writeSpace();
34063438
emitList(node, node.declarations, ListFormat.VariableDeclarationList);
34073439
}
34083440

3441+
34093442
function emitFunctionDeclaration(node: FunctionDeclaration) {
34103443
emitFunctionDeclarationOrExpression(node);
34113444
}

src/compiler/transformers/declarations.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import {
111111
isFunctionDeclaration,
112112
isFunctionLike,
113113
isGlobalScopeAugmentation,
114+
isIdentifier,
114115
isIdentifierText,
115116
isImportEqualsDeclaration,
116117
isIndexSignatureDeclaration,
@@ -140,6 +141,7 @@ import {
140141
isStringLiteralLike,
141142
isTupleTypeNode,
142143
isTypeAliasDeclaration,
144+
isTypeReferenceNode,
143145
isTypeElement,
144146
isTypeLiteralNode,
145147
isTypeNode,
@@ -217,6 +219,18 @@ import {
217219
VisitResult,
218220
} from "../_namespaces/ts.js";
219221

222+
223+
function isUniqueSymbolByType(type: TypeNode | undefined): boolean {
224+
if (!type) return false;
225+
226+
if (type.kind === SyntaxKind.TypeOperator && (type as any).operator === SyntaxKind.UniqueKeyword) return true;
227+
228+
if (isTypeReferenceNode(type) && isIdentifier(type.typeName)) {
229+
if (type.typeName.escapedText === "unique symbol") return true;
230+
}
231+
return false;
232+
}
233+
220234
/** @internal */
221235
export function getDeclarationDiagnostics(
222236
host: EmitHost,
@@ -1479,7 +1493,15 @@ export function transformDeclarations(context: TransformationContext): Transform
14791493
exportMappings.push([name, nameStr]);
14801494
}
14811495
const varDecl = factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, /*initializer*/ undefined);
1482-
return factory.createVariableStatement(isNonContextualKeywordName ? undefined : [factory.createToken(SyntaxKind.ExportKeyword)], factory.createVariableDeclarationList([varDecl]));
1496+
const shouldForceConst = isUniqueSymbolByType(type);
1497+
1498+
const declList = factory.createVariableDeclarationList(
1499+
[varDecl],
1500+
shouldForceConst ? NodeFlags.Const : NodeFlags.None
1501+
);
1502+
1503+
return factory.createVariableStatement(
1504+
isNonContextualKeywordName ? undefined : [factory.createToken(SyntaxKind.ExportKeyword)],declList);
14831505
});
14841506
if (!exportMappings.length) {
14851507
declarations = mapDefined(declarations, declaration => factory.replaceModifiers(declaration, ModifierFlags.None));
@@ -1971,4 +1993,4 @@ function isProcessedComponent(node: Node): node is ProcessedComponent {
19711993
return true;
19721994
}
19731995
return false;
1974-
}
1996+
}

tests/baselines/reference/api/typescript.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9499,9 +9499,10 @@ declare namespace ts {
94999499
* @param context A lexical environment context for the visitor.
95009500
*/
95019501
function visitEachChild<T extends Node>(node: T | undefined, visitor: Visitor, context: TransformationContext | undefined, nodesVisitor?: typeof visitNodes, tokenVisitor?: Visitor): T | undefined;
9502+
function isBuildInfoFile(file: string): boolean;
95029503
function getTsBuildInfoEmitOutputFilePath(options: CompilerOptions): string | undefined;
95039504
function getOutputFileNames(commandLine: ParsedCommandLine, inputFileName: string, ignoreCase: boolean): readonly string[];
9504-
function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer;
9505+
function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers, typeChecker?: TypeChecker): Printer;
95059506
enum ProgramUpdateLevel {
95069507
/** Program is updated with same root file names and options */
95079508
Update = 0,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
uniqueSymbolReassignment.ts(2,18): error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
2+
uniqueSymbolReassignment.ts(7,23): error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
3+
uniqueSymbolReassignment.ts(19,26): error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
4+
uniqueSymbolReassignment.ts(20,26): error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
5+
6+
7+
==== uniqueSymbolReassignment.ts (4 errors) ====
8+
// This is a unique symbol (const + Symbol())
9+
const mySymbol = Symbol('Symbols.mySymbol');
10+
~~~~~~
11+
!!! error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
12+
const Symbols = {
13+
mySymbol
14+
} as const;
15+
16+
const anotherUnique = Symbol('symbols.anotherUnique');
17+
~~~~~~
18+
!!! error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
19+
const Symbols2 = {
20+
anotherUnique
21+
} as const;
22+
23+
function myFunction() {}
24+
25+
// Attach the unique ones
26+
myFunction.mySymbol = Symbols.mySymbol;
27+
myFunction.anotherUnique = Symbols2.anotherUnique;
28+
29+
// Non-unique symbols (regular Symbol() without const)
30+
const nonUniqueSymbol1 = Symbol('nonUnique1');
31+
~~~~~~
32+
!!! error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
33+
const nonUniqueSymbol2 = Symbol('nonUnique2');
34+
~~~~~~
35+
!!! error TS2585: 'Symbol' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.
36+
37+
// Plain text variables (not symbols at all)
38+
const normalVar = "not a symbol";
39+
const symbolName = "this contains symbol but is not one";
40+
41+
// Attach those as well
42+
myFunction.nonUnique1 = nonUniqueSymbol1;
43+
myFunction.nonUnique2 = nonUniqueSymbol2;
44+
myFunction.normalVar = normalVar;
45+
myFunction.symbolName = symbolName;
46+
47+
export { myFunction };
48+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//// [tests/cases/compiler/uniqueSymbolReassignment.ts] ////
2+
3+
//// [uniqueSymbolReassignment.ts]
4+
// This is a unique symbol (const + Symbol())
5+
const mySymbol = Symbol('Symbols.mySymbol');
6+
const Symbols = {
7+
mySymbol
8+
} as const;
9+
10+
const anotherUnique = Symbol('symbols.anotherUnique');
11+
const Symbols2 = {
12+
anotherUnique
13+
} as const;
14+
15+
function myFunction() {}
16+
17+
// Attach the unique ones
18+
myFunction.mySymbol = Symbols.mySymbol;
19+
myFunction.anotherUnique = Symbols2.anotherUnique;
20+
21+
// Non-unique symbols (regular Symbol() without const)
22+
const nonUniqueSymbol1 = Symbol('nonUnique1');
23+
const nonUniqueSymbol2 = Symbol('nonUnique2');
24+
25+
// Plain text variables (not symbols at all)
26+
const normalVar = "not a symbol";
27+
const symbolName = "this contains symbol but is not one";
28+
29+
// Attach those as well
30+
myFunction.nonUnique1 = nonUniqueSymbol1;
31+
myFunction.nonUnique2 = nonUniqueSymbol2;
32+
myFunction.normalVar = normalVar;
33+
myFunction.symbolName = symbolName;
34+
35+
export { myFunction };
36+
37+
38+
//// [uniqueSymbolReassignment.js]
39+
"use strict";
40+
Object.defineProperty(exports, "__esModule", { value: true });
41+
exports.myFunction = myFunction;
42+
// This is a unique symbol (const + Symbol())
43+
var mySymbol = Symbol('Symbols.mySymbol');
44+
var Symbols = {
45+
mySymbol: mySymbol
46+
};
47+
var anotherUnique = Symbol('symbols.anotherUnique');
48+
var Symbols2 = {
49+
anotherUnique: anotherUnique
50+
};
51+
function myFunction() { }
52+
// Attach the unique ones
53+
myFunction.mySymbol = Symbols.mySymbol;
54+
myFunction.anotherUnique = Symbols2.anotherUnique;
55+
// Non-unique symbols (regular Symbol() without const)
56+
var nonUniqueSymbol1 = Symbol('nonUnique1');
57+
var nonUniqueSymbol2 = Symbol('nonUnique2');
58+
// Plain text variables (not symbols at all)
59+
var normalVar = "not a symbol";
60+
var symbolName = "this contains symbol but is not one";
61+
// Attach those as well
62+
myFunction.nonUnique1 = nonUniqueSymbol1;
63+
myFunction.nonUnique2 = nonUniqueSymbol2;
64+
myFunction.normalVar = normalVar;
65+
myFunction.symbolName = symbolName;

0 commit comments

Comments
 (0)