Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Distribute 'keyof' union types #23626

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
436 changes: 239 additions & 197 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,12 @@ namespace ts {
category: Diagnostics.Advanced_Options,
description: Diagnostics.Disable_strict_checking_of_generic_signatures_in_function_types,
},
{
name: "keyofStringsOnly",
type: "boolean",
category: Diagnostics.Advanced_Options,
description: Diagnostics.Resolve_keyof_to_string_valued_property_names_only_no_numbers_or_symbols,
},
{
// A list of plugins to load in the language service
name: "plugins",
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3526,6 +3526,10 @@
"category": "Message",
"code": 6194
},
"Resolve 'keyof' to string valued property names only (no numbers or symbols).": {
"category": "Message",
"code": 6195
},
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005
Expand Down
22 changes: 12 additions & 10 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3377,6 +3377,7 @@ namespace ts {
/* @internal */ mergeId?: number; // Merge id (used to look up merged symbol)
/* @internal */ parent?: Symbol; // Parent symbol
/* @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol
/* @internal */ nameType?: Type; // Type associated with a late-bound symbol
/* @internal */ constEnumOnlyModule?: boolean; // True if module contains only const enums or other modules with only const enums
/* @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter.
/* @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol?
Expand Down Expand Up @@ -3411,7 +3412,6 @@ namespace ts {
enumKind?: EnumKind; // Enum declaration classification
originatingImport?: ImportDeclaration | ImportCall; // Import declaration which produced the symbol, present if the symbol is marked as uncallable but had call signatures in `resolveESModuleSymbol`
lateSymbol?: Symbol; // Late-bound symbol for a computed property
nameType?: Type; // Type associate with a late-bound or mapped type property symbol's name
}

/* @internal */
Expand Down Expand Up @@ -3608,11 +3608,12 @@ namespace ts {
Intrinsic = Any | String | Number | Boolean | BooleanLiteral | ESSymbol | Void | Undefined | Null | Never | NonPrimitive,
/* @internal */
Primitive = String | Number | Boolean | Enum | EnumLiteral | ESSymbol | Void | Undefined | Null | Literal | UniqueESSymbol,
StringLike = String | StringLiteral | Index,
StringLike = String | StringLiteral,
NumberLike = Number | NumberLiteral | Enum,
BooleanLike = Boolean | BooleanLiteral,
EnumLike = Enum | EnumLiteral,
ESSymbolLike = ESSymbol | UniqueESSymbol,
VoidLike = Void | Undefined,
UnionOrIntersection = Union | Intersection,
StructuredType = Object | Union | Intersection,
TypeVariable = TypeParameter | IndexedAccess,
Expand Down Expand Up @@ -3765,7 +3766,7 @@ namespace ts {
/* @internal */
resolvedIndexType: IndexType;
/* @internal */
resolvedDeclaredIndexType: IndexType;
resolvedStringIndexType: IndexType;
/* @internal */
resolvedBaseConstraint: Type;
/* @internal */
Expand Down Expand Up @@ -3854,7 +3855,7 @@ namespace ts {
/* @internal */
resolvedIndexType?: IndexType;
/* @internal */
resolvedDeclaredIndexType?: IndexType;
resolvedStringIndexType?: IndexType;
}

// Type parameters (TypeFlags.TypeParameter)
Expand Down Expand Up @@ -3886,9 +3887,9 @@ namespace ts {

// keyof T types (TypeFlags.Index)
export interface IndexType extends InstantiableType {
/* @internal */
isDeclaredType?: boolean;
type: InstantiableType | UnionOrIntersectionType;
/* @internal */
stringsOnly: boolean;
}

export interface ConditionalRoot {
Expand Down Expand Up @@ -4047,10 +4048,10 @@ namespace ts {

/* @internal */
export interface WideningContext {
parent?: WideningContext; // Parent context
propertyName?: __String; // Name of property in parent
siblings?: Type[]; // Types of siblings
resolvedPropertyNames?: __String[]; // Property names occurring in sibling object literals
parent?: WideningContext; // Parent context
propertyName?: __String; // Name of property in parent
siblings?: Type[]; // Types of siblings
resolvedProperties?: Symbol[]; // Properties occurring in sibling object literals
}

/* @internal */
Expand Down Expand Up @@ -4165,6 +4166,7 @@ namespace ts {
inlineSources?: boolean;
isolatedModules?: boolean;
jsx?: JsxEmit;
keyofStringsOnly?: boolean;
lib?: string[];
/*@internal*/listEmittedFiles?: boolean;
/*@internal*/listFiles?: boolean;
Expand Down
17 changes: 3 additions & 14 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,10 @@ namespace FourSlash {
if (configFileName) {
const baseDir = ts.normalizePath(ts.getDirectoryPath(configFileName));
const host = new Utils.MockParseConfigHost(baseDir, /*ignoreCase*/ false, this.inputFiles);

const configJsonObj = ts.parseConfigFileTextToJson(configFileName, this.inputFiles.get(configFileName));
assert.isTrue(configJsonObj.config !== undefined);

compilationOptions = ts.parseJsonConfigFileContent(configJsonObj.config, host, baseDir, compilationOptions, configFileName).options;
const jsonSourceFile = ts.parseJsonText(configFileName, this.inputFiles.get(configFileName));
compilationOptions = ts.parseJsonSourceFileConfigFileContent(jsonSourceFile, host, baseDir, compilationOptions, configFileName).options;
}


if (compilationOptions.typeRoots) {
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
}
Expand Down Expand Up @@ -2423,14 +2419,7 @@ Actual: ${stringify(fullActual)}`);
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
this.goToMarker(markerName);

const actualCompletion = this.getCompletionListAtCaret({ ...ts.defaultPreferences, includeCompletionsForModuleExports: true }).entries.find(e =>
e.name === options.name && e.source === options.source);

if (!actualCompletion.hasAction) {
this.raiseError(`Completion for ${options.name} does not have an associated action.`);
}

const details = this.getCompletionEntryDetails(options.name, actualCompletion.source, options.preferences);
const details = this.getCompletionEntryDetails(options.name, options.source, options.preferences);
if (details.codeActions.length !== 1) {
this.raiseError(`Expected one code action, got ${details.codeActions.length}`);
}
Expand Down
35 changes: 0 additions & 35 deletions src/lib/es2015.core.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
declare type PropertyKey = string | number | symbol;

interface Array<T> {
/**
* Returns the value of the first element in the array where predicate is true, and undefined
Expand Down Expand Up @@ -258,20 +256,6 @@ interface NumberConstructor {
parseInt(string: string, radix?: number): number;
}

interface Object {
/**
* Determines whether an object has a property with the specified name.
* @param v A property name.
*/
hasOwnProperty(v: PropertyKey): boolean;

/**
* Determines whether a specified property is enumerable.
* @param v A property name.
*/
propertyIsEnumerable(v: PropertyKey): boolean;
}

interface ObjectConstructor {
/**
* Copy the values of all of the enumerable own properties from one or more source objects to a
Expand Down Expand Up @@ -327,25 +311,6 @@ interface ObjectConstructor {
* @param proto The value of the new prototype or null.
*/
setPrototypeOf(o: any, proto: object | null): any;

/**
* Gets the own property descriptor of the specified object.
* An own property descriptor is one that is defined directly on the object and is not
* inherited from the object's prototype.
* @param o Object that contains the property.
* @param p Name of the property.
*/
getOwnPropertyDescriptor(o: any, propertyKey: PropertyKey): PropertyDescriptor | undefined;

/**
* Adds a property to an object, or modifies attributes of an existing property.
* @param o Object on which to add or modify the property. This can be a native JavaScript
* object (that is, a user-defined object or a built in object) or a DOM object.
* @param p The property name.
* @param attributes Descriptor for the property. It can be for a data property or an accessor
* property.
*/
defineProperty(o: any, propertyKey: PropertyKey, attributes: PropertyDescriptor): any;
}

interface ReadonlyArray<T> {
Expand Down
12 changes: 7 additions & 5 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ declare function escape(string: string): string;
*/
declare function unescape(string: string): string;

declare type PropertyKey = string | number | symbol;

interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
Expand Down Expand Up @@ -104,7 +106,7 @@ interface Object {
* Determines whether an object has a property with the specified name.
* @param v A property name.
*/
hasOwnProperty(v: string): boolean;
hasOwnProperty(v: PropertyKey): boolean;

/**
* Determines whether an object exists in another object's prototype chain.
Expand All @@ -116,7 +118,7 @@ interface Object {
* Determines whether a specified property is enumerable.
* @param v A property name.
*/
propertyIsEnumerable(v: string): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}

interface ObjectConstructor {
Expand All @@ -139,7 +141,7 @@ interface ObjectConstructor {
* @param o Object that contains the property.
* @param p Name of the property.
*/
getOwnPropertyDescriptor(o: any, p: string): PropertyDescriptor | undefined;
getOwnPropertyDescriptor(o: any, p: PropertyKey): PropertyDescriptor | undefined;

/**
* Returns the names of the own properties of an object. The own properties of an object are those that are defined directly
Expand Down Expand Up @@ -167,7 +169,7 @@ interface ObjectConstructor {
* @param p The property name.
* @param attributes Descriptor for the property. It can be for a data property or an accessor property.
*/
defineProperty(o: any, p: string, attributes: PropertyDescriptor & ThisType<any>): any;
defineProperty(o: any, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): any;

/**
* Adds one or more properties to an object, and/or modifies attributes of existing properties.
Expand Down Expand Up @@ -1340,7 +1342,7 @@ type Pick<T, K extends keyof T> = {
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends string, T> = {
type Record<K extends keyof any, T> = {
[P in K]: T;
};

Expand Down
18 changes: 13 additions & 5 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ namespace ts.Completions {
return getLabelCompletionAtPosition(contextToken.parent);
}

const completionData = getCompletionData(program, log, sourceFile, position, preferences);
const completionData = getCompletionData(program, log, sourceFile, position, preferences, /*detailsEntryId*/ undefined);
if (!completionData) {
return undefined;
}
Expand Down Expand Up @@ -486,9 +486,9 @@ namespace ts.Completions {
previousToken: Node;
readonly isJsxInitializer: IsJsxInitializer;
}
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, { name, source }: CompletionEntryIdentifier,
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier,
): SymbolCompletion | { type: "request", request: Request } | { type: "none" } {
const completionData = getCompletionData(program, log, sourceFile, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true });
const completionData = getCompletionData(program, log, sourceFile, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId);
if (!completionData) {
return { type: "none" };
}
Expand All @@ -505,7 +505,9 @@ namespace ts.Completions {
return firstDefined<Symbol, SymbolCompletion>(symbols, (symbol): SymbolCompletion => { // TODO: Shouldn't need return type annotation (GH#12632)
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
const info = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, origin, completionKind);
return info && info.name === name && getSourceFromOrigin(origin) === source ? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer } : undefined;
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer }
: undefined;
}) || { type: "none" };
}

Expand Down Expand Up @@ -755,6 +757,7 @@ namespace ts.Completions {
sourceFile: SourceFile,
position: number,
preferences: Pick<UserPreferences, "includeCompletionsForModuleExports" | "includeCompletionsWithInsertText">,
detailsEntryId: CompletionEntryIdentifier | undefined,
): CompletionData | Request | undefined {
const typeChecker = program.getTypeChecker();

Expand Down Expand Up @@ -1302,6 +1305,11 @@ namespace ts.Completions {
const tokenTextLowerCase = tokenText.toLowerCase();

codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
// Perf -- ignore other modules if this is a request for details
if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) {
return;
}

for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
// Don't add a completion for a re-export, only for the original.
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
Expand All @@ -1320,7 +1328,7 @@ namespace ts.Completions {
}

const origin: SymbolOriginInfo = { type: "export", moduleSymbol, isDefaultExport };
if (stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
if (detailsEntryId || stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
symbols.push(symbol);
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;
}
Expand Down
23 changes: 22 additions & 1 deletion src/services/getEditsForFileRename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,33 @@ namespace ts {
export function getEditsForFileRename(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray<FileTextChanges> {
const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host);
return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => {
updateTsconfigFiles(program, changeTracker, oldFilePath, newFilePath);
for (const { sourceFile, toUpdate } of getImportsToUpdate(program, oldFilePath)) {
const newPath = pathUpdater(isRef(toUpdate) ? toUpdate.fileName : toUpdate.text);
if (newPath !== undefined) {
const range = isRef(toUpdate) ? toUpdate : createTextRange(toUpdate.getStart(sourceFile) + 1, toUpdate.end - 1);
const range = isRef(toUpdate) ? toUpdate : createStringRange(toUpdate, sourceFile);
changeTracker.replaceRangeWithText(sourceFile, range, isRef(toUpdate) ? newPath : removeFileExtension(newPath));
}
}
});
}

function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldFilePath: string, newFilePath: string): void {
const cfg = program.getCompilerOptions().configFile;
if (!cfg) return;
const oldFile = cfg.jsonObject && getFilesEntry(cfg.jsonObject, oldFilePath);
if (oldFile) {
changeTracker.replaceRangeWithText(cfg, createStringRange(oldFile, cfg), newFilePath);
}
}

function getFilesEntry(cfg: ObjectLiteralExpression, fileName: string): StringLiteral | undefined {
const filesProp = find(cfg.properties, (prop): prop is PropertyAssignment =>
isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files");
const files = filesProp && filesProp.initializer;
return files && isArrayLiteralExpression(files) ? find(files.elements, (e): e is StringLiteral => isStringLiteral(e) && e.text === fileName) : undefined;
}

interface ToUpdate {
readonly sourceFile: SourceFile;
readonly toUpdate: StringLiteralLike | FileReference;
Expand Down Expand Up @@ -52,4 +69,8 @@ namespace ts {
return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel)));
};
}

function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange {
return createTextRange(node.getStart(sourceFile) + 1, node.end - 1);
}
}
4 changes: 3 additions & 1 deletion tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2100,11 +2100,12 @@ declare namespace ts {
Unit = 13536,
StringOrNumberLiteral = 96,
PossiblyFalsy = 14574,
StringLike = 524322,
StringLike = 34,
NumberLike = 84,
BooleanLike = 136,
EnumLike = 272,
ESSymbolLike = 1536,
VoidLike = 6144,
UnionOrIntersection = 393216,
StructuredType = 458752,
TypeVariable = 1081344,
Expand Down Expand Up @@ -2340,6 +2341,7 @@ declare namespace ts {
inlineSources?: boolean;
isolatedModules?: boolean;
jsx?: JsxEmit;
keyofStringsOnly?: boolean;
lib?: string[];
locale?: string;
mapRoot?: string;
Expand Down
4 changes: 3 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2100,11 +2100,12 @@ declare namespace ts {
Unit = 13536,
StringOrNumberLiteral = 96,
PossiblyFalsy = 14574,
StringLike = 524322,
StringLike = 34,
NumberLike = 84,
BooleanLike = 136,
EnumLike = 272,
ESSymbolLike = 1536,
VoidLike = 6144,
UnionOrIntersection = 393216,
StructuredType = 458752,
TypeVariable = 1081344,
Expand Down Expand Up @@ -2340,6 +2341,7 @@ declare namespace ts {
inlineSources?: boolean;
isolatedModules?: boolean;
jsx?: JsxEmit;
keyofStringsOnly?: boolean;
lib?: string[];
locale?: string;
mapRoot?: string;
Expand Down
Loading