diff --git a/src/services/completions.ts b/src/services/completions.ts index 6171d25062d..f02367b72c8 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -36,7 +36,7 @@ namespace ts.Completions { ): CompletionInfo | undefined { if (isInReferenceComment(sourceFile, position)) { const entries = PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host); - return entries && pathCompletionsInfo(entries); + return entries && convertPathCompletions(entries); } const contextToken = findPrecedingToken(position, sourceFile); @@ -44,7 +44,7 @@ namespace ts.Completions { if (isInString(sourceFile, position, contextToken)) { return !contextToken || !isStringLiteral(contextToken) && !isNoSubstitutionTemplateLiteral(contextToken) ? undefined - : getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host, log); + : convertStringLiteralCompletions(getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host), sourceFile, typeChecker, log); } if (contextToken && isBreakOrContinueStatement(contextToken.parent) @@ -73,6 +73,34 @@ namespace ts.Completions { } } + function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, sourceFile: SourceFile, checker: TypeChecker, log: Log): CompletionInfo | undefined { + if (completion === undefined) { + return undefined; + } + switch (completion.kind) { + case StringLiteralCompletionKind.Paths: + return convertPathCompletions(completion.paths); + case StringLiteralCompletionKind.Properties: { + const entries: CompletionEntry[] = []; + getCompletionEntriesFromSymbols(completion.symbols, entries, sourceFile, sourceFile, checker, ScriptTarget.ESNext, log, CompletionKind.String); // Target will not be used, so arbitrary + return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries }; + } + case StringLiteralCompletionKind.Types: { + const entries = completion.types.map(type => ({ name: type.value, kindModifiers: ScriptElementKindModifier.none, kind: ScriptElementKind.variableElement, sortText: "0" })); + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: true, entries }; + } + default: + return Debug.assertNever(completion); + } + } + + function convertPathCompletions(pathCompletions: ReadonlyArray): CompletionInfo { + const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment. + const isNewIdentifierLocation = true; // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of. + const entries = pathCompletions.map(({ name, kind, span }) => ({ name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: "0", replacementSpan: span })); + return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries }; + } + function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo { return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; } @@ -294,156 +322,6 @@ namespace ts.Completions { } } - function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined { - switch (node.parent.kind) { - case SyntaxKind.LiteralType: - switch (node.parent.parent.kind) { - case SyntaxKind.TypeReference: - // TODO: GH#21168 - return undefined; - case SyntaxKind.IndexedAccessType: - // Get all apparent property names - // i.e. interface Foo { - // foo: string; - // bar: string; - // } - // let x: Foo["/*completion position*/"] - const type = typeChecker.getTypeFromTypeNode((node.parent.parent as IndexedAccessTypeNode).objectType); - return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log); - default: - return undefined; - } - - case SyntaxKind.PropertyAssignment: - if (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression && - (node.parent).name === node) { - // Get quoted name of properties of the object literal expression - // i.e. interface ConfigFiles { - // 'jspm:dev': string - // } - // let files: ConfigFiles = { - // '/*completion position*/' - // } - // - // function foo(c: ConfigFiles) {} - // foo({ - // '/*completion position*/' - // }); - return getStringLiteralCompletionEntriesFromPropertyAssignment(node.parent, sourceFile, typeChecker, compilerOptions.target, log); - } - return fromContextualType(); - - case SyntaxKind.ElementAccessExpression: { - const { expression, argumentExpression } = node.parent as ElementAccessExpression; - if (node === argumentExpression) { - // Get all names of properties on the expression - // i.e. interface A { - // 'prop1': string - // } - // let a: A; - // a['/*completion position*/'] - const type = typeChecker.getTypeAtLocation(expression); - return getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(node, sourceFile, type, typeChecker, compilerOptions.target, log); - } - break; - } - - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - if (!isRequireCall(node.parent, /*checkArgumentIsStringLiteral*/ false) && !isImportCall(node.parent)) { - const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile); - // Get string literal completions from specialized signatures of the target - // i.e. declare function f(a: 'A'); - // f("/*completion position*/") - return argumentInfo ? getStringLiteralCompletionEntriesFromCallExpression(argumentInfo, typeChecker) : fromContextualType(); - } - // falls through - - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ExternalModuleReference: - // Get all known external module names or complete a path to a module - // i.e. import * as ns from "/*completion position*/"; - // var y = import("/*completion position*/"); - // import x = require("/*completion position*/"); - // var y = require("/*completion position*/"); - // export * from "/*completion position*/"; - return pathCompletionsInfo(PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker)); - - default: - return fromContextualType(); - } - - function fromContextualType(): CompletionInfo { - // Get completion for string literal from string literal type - // i.e. var x: "hi" | "hello" = "/*completion position*/" - return getStringLiteralCompletionEntriesFromType(getContextualTypeFromParent(node, typeChecker), typeChecker); - } - } - - function pathCompletionsInfo(entries: CompletionEntry[]): CompletionInfo { - return { - // We don't want the editor to offer any other completions, such as snippets, inside a comment. - isGlobalCompletion: false, - isMemberCompletion: false, - // The user may type in a path that doesn't yet exist, creating a "new identifier" - // with respect to the collection of identifiers the server is aware of. - isNewIdentifierLocation: true, - entries, - }; - } - - function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement, sourceFile: SourceFile, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined { - const type = typeChecker.getContextualType((element.parent)); - const entries: CompletionEntry[] = []; - if (type) { - getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, sourceFile, typeChecker, target, log, CompletionKind.String); - if (entries.length) { - return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries }; - } - } - } - - function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo, typeChecker: TypeChecker): CompletionInfo | undefined { - const candidates: Signature[] = []; - const entries: CompletionEntry[] = []; - const uniques = createMap(); - - typeChecker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount); - - for (const candidate of candidates) { - addStringLiteralCompletionsFromType(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), entries, typeChecker, uniques); - } - - if (entries.length) { - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: true, entries }; - } - - return undefined; - } - - function getStringLiteralCompletionEntriesFromElementAccessOrIndexedAccess(stringLiteralNode: StringLiteral | NoSubstitutionTemplateLiteral, sourceFile: SourceFile, type: Type, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined { - const entries: CompletionEntry[] = []; - if (type) { - getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, stringLiteralNode, sourceFile, typeChecker, target, log, CompletionKind.String); - if (entries.length) { - return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries }; - } - } - return undefined; - } - - function getStringLiteralCompletionEntriesFromType(type: Type, typeChecker: TypeChecker): CompletionInfo | undefined { - if (type) { - const entries: CompletionEntry[] = []; - addStringLiteralCompletionsFromType(type, entries, typeChecker); - if (entries.length) { - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; - } - } - return undefined; - } - function getLabelStatementCompletions(node: Node): CompletionEntry[] { const entries: CompletionEntry[] = []; const uniques = createMap(); @@ -470,30 +348,111 @@ namespace ts.Completions { return entries; } - function addStringLiteralCompletionsFromType(type: Type, result: Push, typeChecker: TypeChecker, uniques = createMap()): void { + const enum StringLiteralCompletionKind { Paths, Properties, Types } + type StringLiteralCompletion = + | { readonly kind: StringLiteralCompletionKind.Paths, readonly paths: ReadonlyArray } + | { readonly kind: StringLiteralCompletionKind.Properties, readonly symbols: ReadonlyArray } + | { readonly kind: StringLiteralCompletionKind.Types, readonly types: ReadonlyArray }; + function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost): StringLiteralCompletion | undefined { + switch (node.parent.kind) { + case SyntaxKind.LiteralType: + switch (node.parent.parent.kind) { + case SyntaxKind.TypeReference: + // TODO: GH#21168 + return undefined; + case SyntaxKind.IndexedAccessType: + // Get all apparent property names + // i.e. interface Foo { + // foo: string; + // bar: string; + // } + // let x: Foo["/*completion position*/"] + return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeFromTypeNode((node.parent.parent as IndexedAccessTypeNode).objectType).getApparentProperties() }; + default: + return undefined; + } + + case SyntaxKind.PropertyAssignment: + if (isObjectLiteralExpression(node.parent.parent) && (node.parent).name === node) { + // Get quoted name of properties of the object literal expression + // i.e. interface ConfigFiles { + // 'jspm:dev': string + // } + // let files: ConfigFiles = { + // '/*completion position*/' + // } + // + // function foo(c: ConfigFiles) {} + // foo({ + // '/*completion position*/' + // }); + const type = typeChecker.getContextualType(node.parent.parent); + return { kind: StringLiteralCompletionKind.Properties, symbols: type && type.getApparentProperties() }; + } + return fromContextualType(); + + case SyntaxKind.ElementAccessExpression: { + const { expression, argumentExpression } = node.parent as ElementAccessExpression; + if (node === argumentExpression) { + // Get all names of properties on the expression + // i.e. interface A { + // 'prop1': string + // } + // let a: A; + // a['/*completion position*/'] + return { kind: StringLiteralCompletionKind.Properties, symbols: typeChecker.getTypeAtLocation(expression).getApparentProperties() }; + } + return undefined; + } + + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + if (!isRequireCall(node.parent, /*checkArgumentIsStringLiteral*/ false) && !isImportCall(node.parent)) { + const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile); + // Get string literal completions from specialized signatures of the target + // i.e. declare function f(a: 'A'); + // f("/*completion position*/") + if (argumentInfo) { + const candidates: Signature[] = []; + typeChecker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount); + const uniques = createMap(); + return { kind: StringLiteralCompletionKind.Types, types: flatMap(candidates, candidate => getStringLiteralTypes(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), typeChecker, uniques)) }; + } + return fromContextualType(); + } + // falls through (is `require("")` or `import("")`) + + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ExternalModuleReference: + // Get all known external module names or complete a path to a module + // i.e. import * as ns from "/*completion position*/"; + // var y = import("/*completion position*/"); + // import x = require("/*completion position*/"); + // var y = require("/*completion position*/"); + // export * from "/*completion position*/"; + return { kind: StringLiteralCompletionKind.Paths, paths: PathCompletions.getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) }; + + default: + return fromContextualType(); + } + + function fromContextualType(): StringLiteralCompletion { + // Get completion for string literal from string literal type + // i.e. var x: "hi" | "hello" = "/*completion position*/" + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker), typeChecker) }; + } + } + + function getStringLiteralTypes(type: Type, typeChecker: TypeChecker, uniques = createMap()): ReadonlyArray { if (type && type.flags & TypeFlags.TypeParameter) { - type = typeChecker.getBaseConstraintOfType(type); - } - if (!type) { - return; - } - if (type.flags & TypeFlags.Union) { - for (const t of (type).types) { - addStringLiteralCompletionsFromType(t, result, typeChecker, uniques); - } - } - else if (type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral)) { - const name = (type).value; - if (!uniques.has(name)) { - uniques.set(name, true); - result.push({ - name, - kindModifiers: ScriptElementKindModifier.none, - kind: ScriptElementKind.variableElement, - sortText: "0" - }); - } + type = type.getConstraint(); } + return type && type.flags & TypeFlags.Union + ? flatMap((type).types, t => getStringLiteralTypes(t, typeChecker, uniques)) + : type && type.flags & TypeFlags.StringLiteral && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, (type as StringLiteralType).value) + ? [type as StringLiteralType] + : emptyArray; } interface SymbolCompletion { diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index 32b7f94c88b..7d5fd296cc6 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -1,6 +1,17 @@ /* @internal */ namespace ts.Completions.PathCompletions { - export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] { + export interface NameAndKind { + readonly name: string; + readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName; + } + export interface PathCompletion extends NameAndKind { + readonly span: TextSpan; + } + function createPathCompletion(name: string, kind: PathCompletion["kind"], span: TextSpan): PathCompletion { + return { name, kind, span }; + } + + export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): PathCompletion[] { const literalValue = normalizeSlashes(node.text); const scriptPath = node.getSourceFile().path; @@ -43,12 +54,12 @@ namespace ts.Completions.PathCompletions { compareStringsCaseSensitive); } - function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: ReadonlyArray, includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): CompletionEntry[] { + function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: ReadonlyArray, includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): PathCompletion[] { const basePath = compilerOptions.project || host.getCurrentDirectory(); const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptPath, ignoreCase); - const result: CompletionEntry[] = []; + const result: PathCompletion[] = []; for (const baseDirectory of baseDirectories) { getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensions, includeExtensions, span, host, exclude, result); @@ -60,7 +71,7 @@ namespace ts.Completions.PathCompletions { /** * Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename. */ - function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: ReadonlyArray, includeExtensions: boolean, span: TextSpan, host: LanguageServiceHost, exclude?: string, result: CompletionEntry[] = []): CompletionEntry[] { + function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: ReadonlyArray, includeExtensions: boolean, span: TextSpan, host: LanguageServiceHost, exclude?: string, result: PathCompletion[] = []): PathCompletion[] { if (fragment === undefined) { fragment = ""; } @@ -109,7 +120,7 @@ namespace ts.Completions.PathCompletions { } forEachKey(foundFiles, foundFile => { - result.push(createCompletionEntryForModule(foundFile, ScriptElementKind.scriptElement, span)); + result.push(createPathCompletion(foundFile, ScriptElementKind.scriptElement, span)); }); } @@ -120,7 +131,7 @@ namespace ts.Completions.PathCompletions { for (const directory of directories) { const directoryName = getBaseFileName(normalizePath(directory)); - result.push(createCompletionEntryForModule(directoryName, ScriptElementKind.directory, span)); + result.push(createPathCompletion(directoryName, ScriptElementKind.directory, span)); } } } @@ -135,10 +146,10 @@ namespace ts.Completions.PathCompletions { * Modules from node_modules (i.e. those listed in package.json) * This includes all files that are found in node_modules/moduleName/ with acceptable file extensions */ - function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] { + function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): PathCompletion[] { const { baseUrl, paths } = compilerOptions; - const result: CompletionEntry[] = []; + const result: PathCompletion[] = []; const fileExtensions = getSupportedExtensions(compilerOptions); if (baseUrl) { @@ -149,10 +160,10 @@ namespace ts.Completions.PathCompletions { for (const path in paths) { const patterns = paths[path]; if (paths.hasOwnProperty(path) && patterns) { - for (const pathCompletion of getCompletionsForPathMapping(path, patterns, fragment, baseUrl, fileExtensions, host)) { + for (const { name, kind } of getCompletionsForPathMapping(path, patterns, fragment, baseUrl, fileExtensions, host)) { // Path mappings may provide a duplicate way to get to something we've already added, so don't add again. - if (!result.some(entry => entry.name === pathCompletion)) { - result.push(createCompletionEntryForModule(pathCompletion, ScriptElementKind.externalModuleName, span)); + if (!result.some(entry => entry.name === name)) { + result.push(createPathCompletion(name, kind, span)); } } } @@ -171,7 +182,7 @@ namespace ts.Completions.PathCompletions { getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span, result); for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions, typeChecker, host)) { - result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span)); + result.push(createPathCompletion(moduleName, ScriptElementKind.externalModuleName, span)); } return result; @@ -179,22 +190,22 @@ namespace ts.Completions.PathCompletions { function getCompletionsForPathMapping( path: string, patterns: ReadonlyArray, fragment: string, baseUrl: string, fileExtensions: ReadonlyArray, host: LanguageServiceHost, - ): ReadonlyArray { + ): ReadonlyArray { if (!endsWith(path, "*")) { // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion. - return !stringContains(path, "*") && startsWith(path, fragment) ? [path] : emptyArray; + return !stringContains(path, "*") && startsWith(path, fragment) ? [{ name: path, kind: ScriptElementKind.directory }] : emptyArray; } const pathPrefix = path.slice(0, path.length - 1); if (!startsWith(fragment, pathPrefix)) { - return [pathPrefix]; + return [{ name: pathPrefix, kind: ScriptElementKind.directory }]; } const remainingFragment = fragment.slice(pathPrefix.length); return flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host)); } - function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: ReadonlyArray, host: LanguageServiceHost): string[] | undefined { + function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: ReadonlyArray, host: LanguageServiceHost): ReadonlyArray | undefined { if (!host.readDirectory) { return undefined; } @@ -225,14 +236,14 @@ namespace ts.Completions.PathCompletions { // doesn't support. For now, this is safer but slower const includeGlob = normalizedSuffix ? "**/*" : "./*"; - const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]); - const directories = tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)); + const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]).map(name => ({ name, kind: ScriptElementKind.scriptElement })); + const directories = tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)).map(name => ({ name, kind: ScriptElementKind.directory })); // Trim away prefix and suffix - return mapDefined(concatenate(matches, directories), match => { - const normalizedMatch = normalizePath(match); + return mapDefined(concatenate(matches, directories), ({ name, kind }) => { + const normalizedMatch = normalizePath(name); const inner = withoutStartAndEnd(normalizedMatch, completePrefix, normalizedSuffix); - return inner !== undefined ? removeLeadingDirectorySeparator(removeFileExtension(inner)) : undefined; + return inner !== undefined ? { name: removeLeadingDirectorySeparator(removeFileExtension(inner)), kind } : undefined; }); } @@ -285,7 +296,7 @@ namespace ts.Completions.PathCompletions { return deduplicate(nonRelativeModuleNames, equateStringsCaseSensitive, compareStringsCaseSensitive); } - export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): CompletionEntry[] | undefined { + export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): PathCompletion[] | undefined { const token = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos); const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end); @@ -316,7 +327,7 @@ namespace ts.Completions.PathCompletions { } } - function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: CompletionEntry[] = []): CompletionEntry[] { + function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: PathCompletion[] = []): PathCompletion[] { // Check for typings specified in compiler options const seen = createMap(); if (options.types) { @@ -364,7 +375,7 @@ namespace ts.Completions.PathCompletions { function pushResult(moduleName: string) { if (!seen.has(moduleName)) { - result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span)); + result.push(createPathCompletion(moduleName, ScriptElementKind.externalModuleName, span)); seen.set(moduleName, true); } } @@ -433,10 +444,6 @@ namespace ts.Completions.PathCompletions { } } - function createCompletionEntryForModule(name: string, kind: ScriptElementKind, replacementSpan: TextSpan): CompletionEntry { - return { name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: name, replacementSpan }; - } - // Replace everything after the last directory seperator that appears function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan { const index = text.lastIndexOf(directorySeparator); @@ -483,8 +490,8 @@ namespace ts.Completions.PathCompletions { return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; } - function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray): string[] | undefined | undefined { - return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include); + function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray): ReadonlyArray { + return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray; } function tryReadFile(host: LanguageServiceHost, path: string): string | undefined { diff --git a/tests/baselines/reference/inferTypes1.errors.txt b/tests/baselines/reference/inferTypes1.errors.txt index 79a0e430cbd..ff93491dd1d 100644 --- a/tests/baselines/reference/inferTypes1.errors.txt +++ b/tests/baselines/reference/inferTypes1.errors.txt @@ -108,4 +108,36 @@ tests/cases/conformance/types/conditional/inferTypes1.ts(72,43): error TS4081: E !!! error TS2304: Cannot find name 'U'. ~ !!! error TS4081: Exported type alias 'T62' has or is using private name 'U'. + + // Example from #21496 + + type JsonifiedObject = { [K in keyof T]: Jsonified }; + + type Jsonified = + T extends string | number | boolean | null ? T + : T extends undefined | Function ? never // undefined and functions are removed + : T extends { toJSON(): infer R } ? R // toJSON is called if it exists (e.g. Date) + : T extends object ? JsonifiedObject + : "what is this"; + + type Example = { + str: "literalstring", + fn: () => void, + date: Date, + customClass: MyClass, + obj: { + prop: "property", + clz: MyClass, + nested: { attr: Date } + }, + } + + declare class MyClass { + toJSON(): "correct"; + } + + type JsonifiedExample = Jsonified; + declare let ex: JsonifiedExample; + const z1: "correct" = ex.customClass; + const z2: string = ex.obj.nested.attr; \ No newline at end of file diff --git a/tests/baselines/reference/inferTypes1.js b/tests/baselines/reference/inferTypes1.js index 231c39e8952..5a692fe8d05 100644 --- a/tests/baselines/reference/inferTypes1.js +++ b/tests/baselines/reference/inferTypes1.js @@ -71,6 +71,38 @@ type T54 = X3<{ a: (x: number) => void, b: () => void }>; // number type T60 = infer U; // Error type T61 = infer A extends infer B ? infer C : infer D; // Error type T62 = U extends (infer U)[] ? U : U; // Error + +// Example from #21496 + +type JsonifiedObject = { [K in keyof T]: Jsonified }; + +type Jsonified = + T extends string | number | boolean | null ? T + : T extends undefined | Function ? never // undefined and functions are removed + : T extends { toJSON(): infer R } ? R // toJSON is called if it exists (e.g. Date) + : T extends object ? JsonifiedObject + : "what is this"; + +type Example = { + str: "literalstring", + fn: () => void, + date: Date, + customClass: MyClass, + obj: { + prop: "property", + clz: MyClass, + nested: { attr: Date } + }, +} + +declare class MyClass { + toJSON(): "correct"; +} + +type JsonifiedExample = Jsonified; +declare let ex: JsonifiedExample; +const z1: "correct" = ex.customClass; +const z2: string = ex.obj.nested.attr; //// [inferTypes1.js] @@ -85,3 +117,5 @@ var C = /** @class */ (function () { } return C; }()); +var z1 = ex.customClass; +var z2 = ex.obj.nested.attr; diff --git a/tests/baselines/reference/inferTypes1.symbols b/tests/baselines/reference/inferTypes1.symbols index a85f124dede..18674d957a8 100644 --- a/tests/baselines/reference/inferTypes1.symbols +++ b/tests/baselines/reference/inferTypes1.symbols @@ -315,3 +315,106 @@ type T62 = U extends (infer U)[] ? U : U; // Error >U : Symbol(U, Decl(inferTypes1.ts, 71, 30)) >U : Symbol(U, Decl(inferTypes1.ts, 71, 30)) +// Example from #21496 + +type JsonifiedObject = { [K in keyof T]: Jsonified }; +>JsonifiedObject : Symbol(JsonifiedObject, Decl(inferTypes1.ts, 71, 44)) +>T : Symbol(T, Decl(inferTypes1.ts, 75, 21)) +>K : Symbol(K, Decl(inferTypes1.ts, 75, 44)) +>T : Symbol(T, Decl(inferTypes1.ts, 75, 21)) +>Jsonified : Symbol(Jsonified, Decl(inferTypes1.ts, 75, 77)) +>T : Symbol(T, Decl(inferTypes1.ts, 75, 21)) +>K : Symbol(K, Decl(inferTypes1.ts, 75, 44)) + +type Jsonified = +>Jsonified : Symbol(Jsonified, Decl(inferTypes1.ts, 75, 77)) +>T : Symbol(T, Decl(inferTypes1.ts, 77, 15)) + + T extends string | number | boolean | null ? T +>T : Symbol(T, Decl(inferTypes1.ts, 77, 15)) +>T : Symbol(T, Decl(inferTypes1.ts, 77, 15)) + + : T extends undefined | Function ? never // undefined and functions are removed +>T : Symbol(T, Decl(inferTypes1.ts, 77, 15)) +>Function : Symbol(Function, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) + + : T extends { toJSON(): infer R } ? R // toJSON is called if it exists (e.g. Date) +>T : Symbol(T, Decl(inferTypes1.ts, 77, 15)) +>toJSON : Symbol(toJSON, Decl(inferTypes1.ts, 80, 17)) +>R : Symbol(R, Decl(inferTypes1.ts, 80, 33)) +>R : Symbol(R, Decl(inferTypes1.ts, 80, 33)) + + : T extends object ? JsonifiedObject +>T : Symbol(T, Decl(inferTypes1.ts, 77, 15)) +>JsonifiedObject : Symbol(JsonifiedObject, Decl(inferTypes1.ts, 71, 44)) +>T : Symbol(T, Decl(inferTypes1.ts, 77, 15)) + + : "what is this"; + +type Example = { +>Example : Symbol(Example, Decl(inferTypes1.ts, 82, 21)) + + str: "literalstring", +>str : Symbol(str, Decl(inferTypes1.ts, 84, 16)) + + fn: () => void, +>fn : Symbol(fn, Decl(inferTypes1.ts, 85, 25)) + + date: Date, +>date : Symbol(date, Decl(inferTypes1.ts, 86, 19)) +>Date : Symbol(Date, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) + + customClass: MyClass, +>customClass : Symbol(customClass, Decl(inferTypes1.ts, 87, 15)) +>MyClass : Symbol(MyClass, Decl(inferTypes1.ts, 94, 1)) + + obj: { +>obj : Symbol(obj, Decl(inferTypes1.ts, 88, 25)) + + prop: "property", +>prop : Symbol(prop, Decl(inferTypes1.ts, 89, 10)) + + clz: MyClass, +>clz : Symbol(clz, Decl(inferTypes1.ts, 90, 25)) +>MyClass : Symbol(MyClass, Decl(inferTypes1.ts, 94, 1)) + + nested: { attr: Date } +>nested : Symbol(nested, Decl(inferTypes1.ts, 91, 21)) +>attr : Symbol(attr, Decl(inferTypes1.ts, 92, 17)) +>Date : Symbol(Date, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) + + }, +} + +declare class MyClass { +>MyClass : Symbol(MyClass, Decl(inferTypes1.ts, 94, 1)) + + toJSON(): "correct"; +>toJSON : Symbol(MyClass.toJSON, Decl(inferTypes1.ts, 96, 23)) +} + +type JsonifiedExample = Jsonified; +>JsonifiedExample : Symbol(JsonifiedExample, Decl(inferTypes1.ts, 98, 1)) +>Jsonified : Symbol(Jsonified, Decl(inferTypes1.ts, 75, 77)) +>Example : Symbol(Example, Decl(inferTypes1.ts, 82, 21)) + +declare let ex: JsonifiedExample; +>ex : Symbol(ex, Decl(inferTypes1.ts, 101, 11)) +>JsonifiedExample : Symbol(JsonifiedExample, Decl(inferTypes1.ts, 98, 1)) + +const z1: "correct" = ex.customClass; +>z1 : Symbol(z1, Decl(inferTypes1.ts, 102, 5)) +>ex.customClass : Symbol(customClass, Decl(inferTypes1.ts, 87, 15)) +>ex : Symbol(ex, Decl(inferTypes1.ts, 101, 11)) +>customClass : Symbol(customClass, Decl(inferTypes1.ts, 87, 15)) + +const z2: string = ex.obj.nested.attr; +>z2 : Symbol(z2, Decl(inferTypes1.ts, 103, 5)) +>ex.obj.nested.attr : Symbol(attr, Decl(inferTypes1.ts, 92, 17)) +>ex.obj.nested : Symbol(nested, Decl(inferTypes1.ts, 91, 21)) +>ex.obj : Symbol(obj, Decl(inferTypes1.ts, 88, 25)) +>ex : Symbol(ex, Decl(inferTypes1.ts, 101, 11)) +>obj : Symbol(obj, Decl(inferTypes1.ts, 88, 25)) +>nested : Symbol(nested, Decl(inferTypes1.ts, 91, 21)) +>attr : Symbol(attr, Decl(inferTypes1.ts, 92, 17)) + diff --git a/tests/baselines/reference/inferTypes1.types b/tests/baselines/reference/inferTypes1.types index be974f8661a..0b753c30f49 100644 --- a/tests/baselines/reference/inferTypes1.types +++ b/tests/baselines/reference/inferTypes1.types @@ -321,3 +321,107 @@ type T62 = U extends (infer U)[] ? U : U; // Error >U : U >U : No type information available! +// Example from #21496 + +type JsonifiedObject = { [K in keyof T]: Jsonified }; +>JsonifiedObject : JsonifiedObject +>T : T +>K : K +>T : T +>Jsonified : Jsonified +>T : T +>K : K + +type Jsonified = +>Jsonified : Jsonified +>T : T + + T extends string | number | boolean | null ? T +>T : T +>null : null +>T : T + + : T extends undefined | Function ? never // undefined and functions are removed +>T : T +>Function : Function + + : T extends { toJSON(): infer R } ? R // toJSON is called if it exists (e.g. Date) +>T : T +>toJSON : () => R +>R : R +>R : R + + : T extends object ? JsonifiedObject +>T : T +>JsonifiedObject : JsonifiedObject +>T : T + + : "what is this"; + +type Example = { +>Example : Example + + str: "literalstring", +>str : "literalstring" + + fn: () => void, +>fn : () => void + + date: Date, +>date : Date +>Date : Date + + customClass: MyClass, +>customClass : MyClass +>MyClass : MyClass + + obj: { +>obj : { prop: "property"; clz: MyClass; nested: { attr: Date; }; } + + prop: "property", +>prop : "property" + + clz: MyClass, +>clz : MyClass +>MyClass : MyClass + + nested: { attr: Date } +>nested : { attr: Date; } +>attr : Date +>Date : Date + + }, +} + +declare class MyClass { +>MyClass : MyClass + + toJSON(): "correct"; +>toJSON : () => "correct" +} + +type JsonifiedExample = Jsonified; +>JsonifiedExample : JsonifiedObject +>Jsonified : Jsonified +>Example : Example + +declare let ex: JsonifiedExample; +>ex : JsonifiedObject +>JsonifiedExample : JsonifiedObject + +const z1: "correct" = ex.customClass; +>z1 : "correct" +>ex.customClass : "correct" +>ex : JsonifiedObject +>customClass : "correct" + +const z2: string = ex.obj.nested.attr; +>z2 : string +>ex.obj.nested.attr : string +>ex.obj.nested : JsonifiedObject<{ attr: Date; }> +>ex.obj : JsonifiedObject<{ prop: "property"; clz: MyClass; nested: { attr: Date; }; }> +>ex : JsonifiedObject +>obj : JsonifiedObject<{ prop: "property"; clz: MyClass; nested: { attr: Date; }; }> +>nested : JsonifiedObject<{ attr: Date; }> +>attr : string + diff --git a/tests/cases/conformance/types/conditional/inferTypes1.ts b/tests/cases/conformance/types/conditional/inferTypes1.ts index 25552a68acf..6ff7ae67c89 100644 --- a/tests/cases/conformance/types/conditional/inferTypes1.ts +++ b/tests/cases/conformance/types/conditional/inferTypes1.ts @@ -73,3 +73,35 @@ type T54 = X3<{ a: (x: number) => void, b: () => void }>; // number type T60 = infer U; // Error type T61 = infer A extends infer B ? infer C : infer D; // Error type T62 = U extends (infer U)[] ? U : U; // Error + +// Example from #21496 + +type JsonifiedObject = { [K in keyof T]: Jsonified }; + +type Jsonified = + T extends string | number | boolean | null ? T + : T extends undefined | Function ? never // undefined and functions are removed + : T extends { toJSON(): infer R } ? R // toJSON is called if it exists (e.g. Date) + : T extends object ? JsonifiedObject + : "what is this"; + +type Example = { + str: "literalstring", + fn: () => void, + date: Date, + customClass: MyClass, + obj: { + prop: "property", + clz: MyClass, + nested: { attr: Date } + }, +} + +declare class MyClass { + toJSON(): "correct"; +} + +type JsonifiedExample = Jsonified; +declare let ex: JsonifiedExample; +const z1: "correct" = ex.customClass; +const z2: string = ex.obj.nested.attr; diff --git a/tests/cases/fourslash/completionsPaths_kinds.ts b/tests/cases/fourslash/completionsPaths_kinds.ts new file mode 100644 index 00000000000..152c7d1bf5a --- /dev/null +++ b/tests/cases/fourslash/completionsPaths_kinds.ts @@ -0,0 +1,30 @@ +/// + +// @Filename: /src/b.ts +////not read + +// @Filename: /src/dir/x.ts +////not read + +// @Filename: /src/a.ts +////import {} from "./[|/*0*/|]"; +////import {} from "./[|/*1*/|]"; + +// @Filename: /tsconfig.json +////{ +//// "compilerOptions": { +//// "baseUrl": ".", +//// "paths": { +//// "foo/*": ["src/*"] +//// } +//// } +////} + +goTo.marker("0"); +verify.completionListContains("dir", undefined, undefined, "directory"); +verify.completionListContains("b", undefined, undefined, "script"); + +goTo.marker("1"); +verify.completionListContains("dir", undefined, undefined, "directory"); +verify.completionListContains("b", undefined, undefined, "script"); +