Skip to content

Commit

Permalink
fix(50375): Errors for missing enum-named properties should attempt t…
Browse files Browse the repository at this point in the history
…o preserve names (#50382)

* fix(50375): preserve enum-named properties

* add AllowComputedPropertyEnums option

* use bit shifting

* rename AllowComputedPropertyEnum -> WriteComputedProps

* mark WriteComputedProps as internal

* mark symbolToNode as internal
  • Loading branch information
a-tarasyuk committed Aug 24, 2022
1 parent fb717df commit 8d7ad8c
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 13 deletions.
25 changes: 23 additions & 2 deletions src/compiler/checker.ts
Expand Up @@ -464,6 +464,7 @@ namespace ts {
signatureToSignatureDeclaration: nodeBuilder.signatureToSignatureDeclaration,
symbolToEntityName: nodeBuilder.symbolToEntityName,
symbolToExpression: nodeBuilder.symbolToExpression,
symbolToNode: nodeBuilder.symbolToNode,
symbolToTypeParameterDeclarations: nodeBuilder.symbolToTypeParameterDeclarations,
symbolToParameterDeclaration: nodeBuilder.symbolToParameterDeclaration,
typeParameterToDeclaration: nodeBuilder.typeParameterToDeclaration,
Expand Down Expand Up @@ -4822,7 +4823,10 @@ namespace ts {
if (flags & SymbolFormatFlags.DoNotIncludeSymbolChain) {
nodeFlags |= NodeBuilderFlags.DoNotIncludeSymbolChain;
}
const builder = flags & SymbolFormatFlags.AllowAnyNodeKind ? nodeBuilder.symbolToExpression : nodeBuilder.symbolToEntityName;
if (flags & SymbolFormatFlags.WriteComputedProps) {
nodeFlags |= NodeBuilderFlags.WriteComputedProps;
}
const builder = flags & SymbolFormatFlags.AllowAnyNodeKind ? nodeBuilder.symbolToNode : nodeBuilder.symbolToEntityName;
return writer ? symbolToStringWorker(writer).getText() : usingSingleLineStringWriter(symbolToStringWorker);

function symbolToStringWorker(writer: EmitTextWriter) {
Expand Down Expand Up @@ -4919,8 +4923,25 @@ namespace ts {
withContext(enclosingDeclaration, flags, tracker, context => typeParameterToDeclaration(parameter, context)),
symbolTableToDeclarationStatements: (symbolTable: SymbolTable, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker, bundled?: boolean) =>
withContext(enclosingDeclaration, flags, tracker, context => symbolTableToDeclarationStatements(symbolTable, context, bundled)),
symbolToNode: (symbol: Symbol, meaning: SymbolFlags, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) =>
withContext(enclosingDeclaration, flags, tracker, context => symbolToNode(symbol, context, meaning)),
};

function symbolToNode(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags) {
if (context.flags & NodeBuilderFlags.WriteComputedProps) {
if (symbol.valueDeclaration) {
const name = getNameOfDeclaration(symbol.valueDeclaration);
if (name && isComputedPropertyName(name)) return name;
}
const nameType = getSymbolLinks(symbol).nameType;
if (nameType && nameType.flags & (TypeFlags.EnumLiteral | TypeFlags.UniqueESSymbol)) {
context.enclosingDeclaration = nameType.symbol.valueDeclaration;
return factory.createComputedPropertyName(symbolToExpression(nameType.symbol, context, meaning));
}
}
return symbolToExpression(symbol, context, meaning);
}

function withContext<T>(enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined, tracker: SymbolTracker | undefined, cb: (context: NodeBuilderContext) => T): T | undefined {
Debug.assert(enclosingDeclaration === undefined || (enclosingDeclaration.flags & NodeFlags.Synthesized) === 0);
const context: NodeBuilderContext = {
Expand Down Expand Up @@ -20310,7 +20331,7 @@ namespace ts {
shouldSkipElaboration = true; // Retain top-level error for interface implementing issues, otherwise omit it
}
if (props.length === 1) {
const propName = symbolToString(unmatchedProperty);
const propName = symbolToString(unmatchedProperty, /*enclosingDeclaration*/ undefined, SymbolFlags.None, SymbolFormatFlags.AllowAnyNodeKind | SymbolFormatFlags.WriteComputedProps);
reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, ...getTypeNamesForErrorDisplay(source, target));
if (length(unmatchedProperty.declarations)) {
associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations![0], Diagnostics._0_is_declared_here, propName));
Expand Down
18 changes: 12 additions & 6 deletions src/compiler/types.ts
Expand Up @@ -4595,6 +4595,8 @@ namespace ts {
/** Note that the resulting nodes cannot be checked. */
symbolToExpression(symbol: Symbol, meaning: SymbolFlags, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): Expression | undefined;
/** Note that the resulting nodes cannot be checked. */
/* @internal */ symbolToNode(symbol: Symbol, meaning: SymbolFlags, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): Node | undefined;
/** Note that the resulting nodes cannot be checked. */
symbolToTypeParameterDeclarations(symbol: Symbol, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): NodeArray<TypeParameterDeclaration> | undefined;
/** Note that the resulting nodes cannot be checked. */
symbolToParameterDeclaration(symbol: Symbol, enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined): ParameterDeclaration | undefined;
Expand Down Expand Up @@ -4872,6 +4874,7 @@ namespace ts {
AllowEmptyTuple = 1 << 19,
AllowUniqueESSymbolType = 1 << 20,
AllowEmptyIndexInfoType = 1 << 21,
/* @internal */ WriteComputedProps = 1 << 30, // { [E.A]: 1 }

// Errors (cont.)
AllowNodeModulesRelativePaths = 1 << 26,
Expand Down Expand Up @@ -4930,27 +4933,30 @@ namespace ts {
}

export const enum SymbolFormatFlags {
None = 0x00000000,
None = 0,

// Write symbols's type argument if it is instantiated symbol
// eg. class C<T> { p: T } <-- Show p as C<T>.p here
// var a: C<number>;
// var p = a.p; <--- Here p is property of C<number> so show it as C<number>.p instead of just C.p
WriteTypeParametersOrArguments = 0x00000001,
WriteTypeParametersOrArguments = 1 << 0,

// Use only external alias information to get the symbol name in the given context
// eg. module m { export class c { } } import x = m.c;
// When this flag is specified m.c will be used to refer to the class instead of alias symbol x
UseOnlyExternalAliasing = 0x00000002,
UseOnlyExternalAliasing = 1 << 1,

// Build symbol name using any nodes needed, instead of just components of an entity name
AllowAnyNodeKind = 0x00000004,
AllowAnyNodeKind = 1 << 2,

// Prefer aliases which are not directly visible
UseAliasDefinedOutsideCurrentScope = 0x00000008,
UseAliasDefinedOutsideCurrentScope = 1 << 3,

// { [E.A]: 1 }
/* @internal */ WriteComputedProps = 1 << 4,

// Skip building an accessible symbol chain
/* @internal */ DoNotIncludeSymbolChain = 0x00000010,
/* @internal */ DoNotIncludeSymbolChain = 1 << 5,
}

/* @internal */
Expand Down
8 changes: 3 additions & 5 deletions src/services/codefixes/fixAddMissingMember.ts
Expand Up @@ -642,11 +642,9 @@ namespace ts.codefix {
}

function createPropertyNameFromSymbol(symbol: Symbol, target: ScriptTarget, quotePreference: QuotePreference, checker: TypeChecker) {
if (isTransientSymbol(symbol) && symbol.nameType && symbol.nameType.flags & TypeFlags.UniqueESSymbol) {
const expression = checker.symbolToExpression((symbol.nameType as UniqueESSymbolType).symbol, SymbolFlags.Value, symbol.valueDeclaration, NodeBuilderFlags.AllowUniqueESSymbolType);
if (expression) {
return factory.createComputedPropertyName(expression);
}
if (isTransientSymbol(symbol)) {
const prop = checker.symbolToNode(symbol, SymbolFlags.Value, /*enclosingDeclaration*/ undefined, NodeBuilderFlags.WriteComputedProps);
if (prop && isComputedPropertyName(prop)) return prop;
}
return createPropertyNameNodeForIdentifierOrLiteral(symbol.name, target, quotePreference === QuotePreference.Single);
}
Expand Down
@@ -0,0 +1,12 @@
tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithEnumIndexer.ts(5,5): error TS2741: Property '[E.A]' is missing in type '{}' but required in type 'Record<E, any>'.


==== tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithEnumIndexer.ts (1 errors) ====
enum E {
A
}

let foo: Record<E, any> = {}
~~~
!!! error TS2741: Property '[E.A]' is missing in type '{}' but required in type 'Record<E, any>'.

14 changes: 14 additions & 0 deletions tests/baselines/reference/assignmentCompatWithEnumIndexer.js
@@ -0,0 +1,14 @@
//// [assignmentCompatWithEnumIndexer.ts]
enum E {
A
}

let foo: Record<E, any> = {}


//// [assignmentCompatWithEnumIndexer.js]
var E;
(function (E) {
E[E["A"] = 0] = "A";
})(E || (E = {}));
var foo = {};
13 changes: 13 additions & 0 deletions tests/baselines/reference/assignmentCompatWithEnumIndexer.symbols
@@ -0,0 +1,13 @@
=== tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithEnumIndexer.ts ===
enum E {
>E : Symbol(E, Decl(assignmentCompatWithEnumIndexer.ts, 0, 0))

A
>A : Symbol(E.A, Decl(assignmentCompatWithEnumIndexer.ts, 0, 8))
}

let foo: Record<E, any> = {}
>foo : Symbol(foo, Decl(assignmentCompatWithEnumIndexer.ts, 4, 3))
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
>E : Symbol(E, Decl(assignmentCompatWithEnumIndexer.ts, 0, 0))

12 changes: 12 additions & 0 deletions tests/baselines/reference/assignmentCompatWithEnumIndexer.types
@@ -0,0 +1,12 @@
=== tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithEnumIndexer.ts ===
enum E {
>E : E

A
>A : E.A
}

let foo: Record<E, any> = {}
>foo : Record<E, any>
>{} : {}

@@ -0,0 +1,5 @@
enum E {
A
}

let foo: Record<E, any> = {}
18 changes: 18 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingProperties23.ts
@@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />

////enum E {
//// A
////}
////let obj: Record<E, any> = {}

verify.codeFix({
index: 0,
description: ts.Diagnostics.Add_missing_properties.message,
newFileContent:
`enum E {
A
}
let obj: Record<E, any> = {
[E.A]: undefined
}`,
});

0 comments on commit 8d7ad8c

Please sign in to comment.