/
githubmirror
/
TypeScript
Обзор
Документация
Войти
/
githubmirror
/
TypeScript
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/services/stringCompletions.ts
1 450 строк
71 KB
Jake Bailey
Fix ContextFlags compile error (#62855)
08 дек 2025, 22:36
Не верифицирован
08 дек 2025, 22:36
3544dd7
Код
Авторство
О чём код?
import { CompletionKind, createCompletionDetails, createCompletionDetailsForSymbol, getCompletionEntriesFromSymbols, getConstraintOfTypeArgumentProperty, getDefaultCommitCharacters, getPropertiesForObjectExpression, Log, SortText, } from "./_namespaces/ts.Completions.js"; import { addToSeen, altDirectorySeparator, arrayFrom, BinaryExpression, CallLikeExpression, CancellationToken, CaseClause, changeExtension, CharacterCodes, combinePaths, comparePaths, comparePatternKeys, compareStringsCaseSensitive, compareValues, Comparison, CompilerOptions, CompletionEntry, CompletionEntryDetails, CompletionInfo, concatenate, contains, containsPath, ContextFlags, createModuleSpecifierResolutionHost, createSortedArray, createTextSpan, createTextSpanFromStringLiteralLikeContent, Debug, deduplicate, directorySeparator, ElementAccessExpression, emptyArray, endsWith, ensureTrailingDirectorySeparator, equateStringsCaseSensitive, escapeString, Extension, fileExtensionIsOneOf, filter, find, findAncestor, findPackageJson, findPackageJsons, firstDefined, firstOrUndefined, flatMap, flatten, forEachAncestorDirectoryStoppingAtGlobalCache, getBaseFileName, getConditions, getContextualTypeFromParent, getDeclarationEmitExtensionForPath, getDirectoryPath, getEffectiveTypeRoots, getEmitModuleResolutionKind, getLeadingCommentRanges, getOwnKeys, getPackageJsonTypesVersionsPaths, getPathComponents, getPathsBasePath, getPossibleOriginalInputExtensionForExtension, getPossibleOriginalInputPathWithoutChangingExt, getReplacementSpanForContextToken, getResolvePackageJsonExports, getResolvePackageJsonImports, getSupportedExtensions, getSupportedExtensionsWithJsonIfResolveJsonModule, getTextOfJsxAttributeName, getTextOfNode, getTokenAtPosition, hasIndexSignature, hasProperty, hasTrailingDirectorySeparator, hostGetCanonicalFileName, hostUsesCaseSensitiveFileNames, ImportOrExportSpecifier, IndexedAccessTypeNode, InternalSymbolName, isApplicableVersionedTypesKey, isArray, isCallExpression, isCallLikeExpression, isIdentifier, isIdentifierText, isImportCall, isInReferenceComment, isInString, isJsxAttribute, isJsxOpeningLikeElement, isLiteralTypeNode, isObjectLiteralExpression, isPatternMatch, isPrivateIdentifierClassElementDeclaration, isRootedDiskPath, isString, isStringLiteral, isStringLiteralLike, isUrl, JsxAttribute, LanguageServiceHost, length, LiteralExpression, LiteralTypeNode, mapDefined, MapLike, moduleExportNameTextEscaped, moduleResolutionUsesNodeModules, ModuleSpecifierEnding, ModuleSpecifierResolutionHost, moduleSpecifiers, newCaseClauseTracker, Node, normalizePath, normalizeSlashes, ObjectLiteralExpression, Path, Program, PropertyAssignment, rangeContainsPosition, readJson, removeFileExtension, removePrefix, removeTrailingDirectorySeparator, ResolutionMode, resolvePath, ScriptElementKind, ScriptElementKindModifier, ScriptTarget, signatureHasRestParameter, SignatureHelp, singleElementArray, skipConstraint, skipParentheses, SourceFile, startsWith, StringLiteralLike, StringLiteralType, stripQuotes, supportedTSImplementationExtensions, Symbol, SyntaxKind, textPart, TextSpan, tryAndIgnoreErrors, tryDirectoryExists, tryFileExists, tryGetDirectories, tryGetExtensionFromPath, tryParsePattern, tryReadDirectory, tryRemoveDirectoryPrefix, tryRemovePrefix, Type, TypeChecker, TypeFlags, UnionTypeNode, unmangleScopedPackageName, UserPreferences, walkUpParenthesizedExpressions, walkUpParenthesizedTypes, } from "./_namespaces/ts.js"; interface NameAndKindSet { add(value: NameAndKind): void; has(name: string): boolean; values(): IterableIterator<NameAndKind>; } const kindPrecedence = { [ScriptElementKind.directory]: 0, [ScriptElementKind.scriptElement]: 1, [ScriptElementKind.externalModuleName]: 2, }; function createNameAndKindSet(): NameAndKindSet { const map = new Map<string, NameAndKind>(); function add(value: NameAndKind) { const existing = map.get(value.name); if (!existing || kindPrecedence[existing.kind] < kindPrecedence[value.kind]) { map.set(value.name, value); } } return { add, has: map.has.bind(map), values: map.values.bind(map), }; } /** @internal */ export function getStringLiteralCompletions( sourceFile: SourceFile, position: number, contextToken: Node | undefined, options: CompilerOptions, host: LanguageServiceHost, program: Program, log: Log, preferences: UserPreferences, includeSymbol: boolean, ): CompletionInfo | undefined { if (isInReferenceComment(sourceFile, position)) { const entries = getTripleSlashReferenceCompletion(sourceFile, position, program, host, createModuleSpecifierResolutionHost(program, host)); return entries && convertPathCompletions(entries); } if (isInString(sourceFile, position, contextToken)) { if (!contextToken || !isStringLiteralLike(contextToken)) return undefined; const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, program, host, preferences); return convertStringLiteralCompletions(entries, contextToken, sourceFile, host, program, log, options, preferences, position, includeSymbol); } } function convertStringLiteralCompletions( completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, host: LanguageServiceHost, program: Program, log: Log, options: CompilerOptions, preferences: UserPreferences, position: number, includeSymbol: boolean, ): CompletionInfo | undefined { if (completion === undefined) { return undefined; } const optionalReplacementSpan = createTextSpanFromStringLiteralLikeContent(contextToken, position); switch (completion.kind) { case StringLiteralCompletionKind.Paths: return convertPathCompletions(completion.paths); case StringLiteralCompletionKind.Properties: { const entries = createSortedArray<CompletionEntry>(); getCompletionEntriesFromSymbols( completion.symbols, entries, contextToken, contextToken, sourceFile, position, sourceFile, host, program, ScriptTarget.ESNext, log, CompletionKind.String, preferences, options, /*formatContext*/ undefined, /*isTypeOnlyLocation*/ undefined, /*propertyAccessToConvert*/ undefined, /*jsxIdentifierExpected*/ undefined, /*isJsxInitializer*/ undefined, /*importStatementCompletion*/ undefined, /*recommendedCompletion*/ undefined, /*symbolToOriginInfoMap*/ undefined, /*symbolToSortTextMap*/ undefined, /*isJsxIdentifierExpected*/ undefined, /*isRightOfOpenTag*/ undefined, includeSymbol, ); // Target will not be used, so arbitrary return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries, defaultCommitCharacters: getDefaultCommitCharacters(completion.hasIndexSignature), }; } case StringLiteralCompletionKind.Types: { const quoteChar = contextToken.kind === SyntaxKind.NoSubstitutionTemplateLiteral ? CharacterCodes.backtick : startsWith(getTextOfNode(contextToken), "'") ? CharacterCodes.singleQuote : CharacterCodes.doubleQuote; const entries = completion.types.map(type => ({ name: escapeString(type.value, quoteChar), kindModifiers: ScriptElementKindModifier.none, kind: ScriptElementKind.string, sortText: SortText.LocationPriority, replacementSpan: getReplacementSpanForContextToken(contextToken, position), commitCharacters: [], })); return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, optionalReplacementSpan, entries, defaultCommitCharacters: getDefaultCommitCharacters(completion.isNewIdentifier), }; } default: return Debug.assertNever(completion); } } /** @internal */ export function getStringLiteralCompletionDetails( name: string, sourceFile: SourceFile, position: number, contextToken: Node | undefined, program: Program, host: LanguageServiceHost, cancellationToken: CancellationToken, preferences: UserPreferences, ): CompletionEntryDetails | undefined { if (!contextToken || !isStringLiteralLike(contextToken)) return undefined; const completions = getStringLiteralCompletionEntries(sourceFile, contextToken, position, program, host, preferences); return completions && stringLiteralCompletionDetails(name, contextToken, completions, sourceFile, program.getTypeChecker(), cancellationToken); } function stringLiteralCompletionDetails(name: string, location: Node, completion: StringLiteralCompletion, sourceFile: SourceFile, checker: TypeChecker, cancellationToken: CancellationToken): CompletionEntryDetails | undefined { switch (completion.kind) { case StringLiteralCompletionKind.Paths: { const match = find(completion.paths, p => p.name === name); return match && createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [textPart(name)]); } case StringLiteralCompletionKind.Properties: { const match = find(completion.symbols, s => s.name === name); return match && createCompletionDetailsForSymbol(match, match.name, checker, sourceFile, location, cancellationToken); } case StringLiteralCompletionKind.Types: return find(completion.types, t => t.value === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.string, [textPart(name)]) : undefined; default: return Debug.assertNever(completion); } } function convertPathCompletions(pathCompletions: readonly PathCompletion[]): 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, extension }): CompletionEntry => ({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: SortText.LocationPriority, replacementSpan: span })); return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries, defaultCommitCharacters: getDefaultCommitCharacters(isNewIdentifierLocation), }; } function kindModifiersFromExtension(extension: Extension | undefined): ScriptElementKindModifier { switch (extension) { case Extension.Dts: return ScriptElementKindModifier.dtsModifier; case Extension.Js: return ScriptElementKindModifier.jsModifier; case Extension.Json: return ScriptElementKindModifier.jsonModifier; case Extension.Jsx: return ScriptElementKindModifier.jsxModifier; case Extension.Ts: return ScriptElementKindModifier.tsModifier; case Extension.Tsx: return ScriptElementKindModifier.tsxModifier; case Extension.Dmts: return ScriptElementKindModifier.dmtsModifier; case Extension.Mjs: return ScriptElementKindModifier.mjsModifier; case Extension.Mts: return ScriptElementKindModifier.mtsModifier; case Extension.Dcts: return ScriptElementKindModifier.dctsModifier; case Extension.Cjs: return ScriptElementKindModifier.cjsModifier; case Extension.Cts: return ScriptElementKindModifier.ctsModifier; case Extension.TsBuildInfo: return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported.`); case undefined: return ScriptElementKindModifier.none; default: return Debug.assertNever(extension); } } const enum StringLiteralCompletionKind { Paths, Properties, Types, } interface StringLiteralCompletionsFromProperties { readonly kind: StringLiteralCompletionKind.Properties; readonly symbols: readonly Symbol[]; readonly hasIndexSignature: boolean; } interface StringLiteralCompletionsFromTypes { readonly kind: StringLiteralCompletionKind.Types; readonly types: readonly StringLiteralType[]; readonly isNewIdentifier: boolean; } type StringLiteralCompletion = { readonly kind: StringLiteralCompletionKind.Paths; readonly paths: readonly PathCompletion[]; } | StringLiteralCompletionsFromProperties | StringLiteralCompletionsFromTypes; function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, program: Program, host: LanguageServiceHost, preferences: UserPreferences): StringLiteralCompletion | undefined { const typeChecker = program.getTypeChecker(); const parent = walkUpParentheses(node.parent); switch (parent.kind) { case SyntaxKind.LiteralType: { const grandParent = walkUpParentheses(parent.parent); if (grandParent.kind === SyntaxKind.ImportType) { return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, program, host, preferences) }; } return fromUnionableLiteralType(grandParent); } case SyntaxKind.PropertyAssignment: if (isObjectLiteralExpression(parent.parent) && (parent as PropertyAssignment).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 stringLiteralCompletionsForObjectLiteral(typeChecker, parent.parent); } if (findAncestor(parent.parent, isCallLikeExpression)) { const uniques = new Set<string>(); const stringLiteralTypes = concatenate( getStringLiteralTypes(typeChecker.getContextualType(node, ContextFlags.None), uniques), getStringLiteralTypes(typeChecker.getContextualType(node, ContextFlags.IgnoreNodeInferences), uniques), ); return toStringLiteralCompletionsFromTypes(stringLiteralTypes); } return fromContextualType(ContextFlags.None); case SyntaxKind.ElementAccessExpression: { const { expression, argumentExpression } = parent as ElementAccessExpression; if (node === skipParentheses(argumentExpression)) { // Get all names of properties on the expression // i.e. interface A { // 'prop1': string // } // let a: A; // a['/*completion position*/'] return stringLiteralCompletionsFromProperties(typeChecker.getTypeAtLocation(expression)); } return undefined; } case SyntaxKind.CallExpression: case SyntaxKind.NewExpression: case SyntaxKind.JsxAttribute: if (!isRequireCallArgument(node) && !isImportCall(parent)) { const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile, typeChecker); // Get string literal completions from specialized signatures of the target // i.e. declare function f(a: 'A'); // f("/*completion position*/") return argumentInfo && getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker) || fromContextualType(ContextFlags.None); } // falls through (is `require("")` or `require(""` or `import("")`) case SyntaxKind.ImportDeclaration: case SyntaxKind.ExportDeclaration: case SyntaxKind.ExternalModuleReference: case SyntaxKind.JSDocImportTag: // 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: getStringLiteralCompletionsFromModuleNames(sourceFile, node, program, host, preferences) }; case SyntaxKind.CaseClause: const tracker = newCaseClauseTracker(typeChecker, (parent as CaseClause).parent.clauses); const contextualTypes = fromContextualType(); if (!contextualTypes) { return; } const literals = contextualTypes.types.filter(literal => !tracker.hasValue(literal.value)); return { kind: StringLiteralCompletionKind.Types, types: literals, isNewIdentifier: false }; case SyntaxKind.ImportSpecifier: case SyntaxKind.ExportSpecifier: // Complete string aliases in `import { "|" } from` and `export { "|" } from` const specifier = parent as ImportOrExportSpecifier; if (specifier.propertyName && node !== specifier.propertyName) { return; // Don't complete in `export { "..." as "|" } from` } const namedImportsOrExports = specifier.parent; const { moduleSpecifier } = namedImportsOrExports.kind === SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent; if (!moduleSpecifier) return; const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier); // TODO: GH#18217 if (!moduleSpecifierSymbol) return; const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol); const existing = new Set(namedImportsOrExports.elements.map(n => moduleExportNameTextEscaped(n.propertyName || n.name))); const uniques = exports.filter(e => e.escapedName !== InternalSymbolName.Default && !existing.has(e.escapedName)); return { kind: StringLiteralCompletionKind.Properties, symbols: uniques, hasIndexSignature: false }; case SyntaxKind.BinaryExpression: if ((parent as BinaryExpression).operatorToken.kind === SyntaxKind.InKeyword) { const type = typeChecker.getTypeAtLocation((parent as BinaryExpression).right); const properties = type.isUnion() ? typeChecker.getAllPossiblePropertiesOfTypes(type.types) : type.getApparentProperties(); return { kind: StringLiteralCompletionKind.Properties, symbols: properties.filter(prop => !prop.valueDeclaration || !isPrivateIdentifierClassElementDeclaration(prop.valueDeclaration)), hasIndexSignature: false, }; } return fromContextualType(ContextFlags.None); default: return fromContextualType() || fromContextualType(ContextFlags.None); } function fromUnionableLiteralType(grandParent: Node): StringLiteralCompletionsFromTypes | StringLiteralCompletionsFromProperties | undefined { switch (grandParent.kind) { case SyntaxKind.CallExpression: case SyntaxKind.ExpressionWithTypeArguments: case SyntaxKind.JsxOpeningElement: case SyntaxKind.JsxSelfClosingElement: case SyntaxKind.NewExpression: case SyntaxKind.TaggedTemplateExpression: case SyntaxKind.TypeReference: { const typeArgument = findAncestor(parent, n => n.parent === grandParent) as LiteralTypeNode; if (typeArgument) { return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(typeArgument)), isNewIdentifier: false }; } return undefined; } case SyntaxKind.IndexedAccessType: // Get all apparent property names // i.e. interface Foo { // foo: string; // bar: string; // } // let x: Foo["/*completion position*/"] const { indexType, objectType } = grandParent as IndexedAccessTypeNode; if (!rangeContainsPosition(indexType, position)) { return undefined; } return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType)); case SyntaxKind.PropertySignature: return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getConstraintOfTypeArgumentProperty(grandParent, typeChecker)), isNewIdentifier: false }; case SyntaxKind.UnionType: { const result = fromUnionableLiteralType(walkUpParentheses(grandParent.parent)); if (!result) { return undefined; } const alreadyUsedTypes = getAlreadyUsedTypesInStringLiteralUnion(grandParent as UnionTypeNode, parent as LiteralTypeNode); if (result.kind === StringLiteralCompletionKind.Properties) { return { kind: StringLiteralCompletionKind.Properties, symbols: result.symbols.filter(sym => !contains(alreadyUsedTypes, sym.name)), hasIndexSignature: result.hasIndexSignature }; } return { kind: StringLiteralCompletionKind.Types, types: result.types.filter(t => !contains(alreadyUsedTypes, t.value)), isNewIdentifier: false }; } default: return undefined; } } function fromContextualType(contextFlags: ContextFlags = ContextFlags.IgnoreNodeInferences): StringLiteralCompletionsFromTypes | undefined { // Get completion for string literal from string literal type // i.e. var x: "hi" | "hello" = "/*completion position*/" return toStringLiteralCompletionsFromTypes(getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker, contextFlags))); } } function toStringLiteralCompletionsFromTypes(types: readonly StringLiteralType[]): StringLiteralCompletionsFromTypes | undefined { return types.length ? { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false } : undefined; } function walkUpParentheses(node: Node) { switch (node.kind) { case SyntaxKind.ParenthesizedType: return walkUpParenthesizedTypes(node); case SyntaxKind.ParenthesizedExpression: return walkUpParenthesizedExpressions(node); default: return node; } } function getAlreadyUsedTypesInStringLiteralUnion(union: UnionTypeNode, current: LiteralTypeNode): readonly string[] { return mapDefined(union.types, type => type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined); } function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg: StringLiteralLike, argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes | undefined { let isNewIdentifier = false; const uniques = new Set<string>(); const editingArgument = isJsxOpeningLikeElement(call) ? Debug.checkDefined(findAncestor(arg.parent, isJsxAttribute)) : arg; const candidates = checker.getCandidateSignaturesForStringLiteralCompletions(call, editingArgument); const types = flatMap(candidates, candidate => { if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return; let type = candidate.getTypeParameterAtPosition(argumentInfo.argumentIndex); if (isJsxOpeningLikeElement(call)) { const propType = checker.getTypeOfPropertyOfType(type, getTextOfJsxAttributeName((editingArgument as JsxAttribute).name)); if (propType) { type = propType; } } isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String); return getStringLiteralTypes(type, uniques); }); return length(types) ? { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier } : undefined; } function stringLiteralCompletionsFromProperties(type: Type | undefined): StringLiteralCompletionsFromProperties | undefined { return type && { kind: StringLiteralCompletionKind.Properties, symbols: filter(type.getApparentProperties(), prop => !(prop.valueDeclaration && isPrivateIdentifierClassElementDeclaration(prop.valueDeclaration))), hasIndexSignature: hasIndexSignature(type), }; } function stringLiteralCompletionsForObjectLiteral(checker: TypeChecker, objectLiteralExpression: ObjectLiteralExpression): StringLiteralCompletionsFromProperties | undefined { const contextualType = checker.getContextualType(objectLiteralExpression); if (!contextualType) return undefined; const completionsType = checker.getContextualType(objectLiteralExpression, ContextFlags.IgnoreNodeInferences); const symbols = getPropertiesForObjectExpression( contextualType, completionsType, objectLiteralExpression, checker, ); return { kind: StringLiteralCompletionKind.Properties, symbols, hasIndexSignature: hasIndexSignature(contextualType), }; } function getStringLiteralTypes(type: Type | undefined, uniques = new Set<string>()): readonly StringLiteralType[] { if (!type) return emptyArray; type = skipConstraint(type); return type.isUnion() ? flatMap(type.types, t => getStringLiteralTypes(t, uniques)) : type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value) ? [type] : emptyArray; } interface NameAndKind { readonly name: string; readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName; readonly extension: Extension | undefined; } interface PathCompletion extends NameAndKind { readonly span: TextSpan | undefined; } function nameAndKind(name: string, kind: NameAndKind["kind"], extension: Extension | undefined): NameAndKind { return { name, kind, extension }; } function directoryResult(name: string): NameAndKind { return nameAndKind(name, ScriptElementKind.directory, /*extension*/ undefined); } function addReplacementSpans(text: string, textStart: number, names: readonly NameAndKind[]): readonly PathCompletion[] { const span = getDirectoryFragmentTextSpan(text, textStart); const wholeSpan = text.length === 0 ? undefined : createTextSpan(textStart, text.length); return names.map(({ name, kind, extension }): PathCompletion => (name.includes(directorySeparator) || name.includes(altDirectorySeparator)) ? { name, kind, extension, span: wholeSpan } : { name, kind, extension, span }); } function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, program: Program, host: LanguageServiceHost, preferences: UserPreferences): readonly PathCompletion[] { return addReplacementSpans(node.text, node.getStart(sourceFile) + 1, getStringLiteralCompletionsFromModuleNamesWorker(sourceFile, node, program, host, preferences)); } function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: SourceFile, node: LiteralExpression, program: Program, host: LanguageServiceHost, preferences: UserPreferences): readonly NameAndKind[] { const literalValue = normalizeSlashes(node.text); const mode = isStringLiteralLike(node) ? program.getModeForUsageLocation(sourceFile, node) : undefined; const scriptPath = sourceFile.path; const scriptDirectory = getDirectoryPath(scriptPath); const compilerOptions = program.getCompilerOptions(); const typeChecker = program.getTypeChecker(); const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); const extensionOptions = getExtensionOptions(compilerOptions, ReferenceKind.ModuleSpecifier, sourceFile, typeChecker, preferences, mode); return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && !compilerOptions.paths && (isRootedDiskPath(literalValue) || isUrl(literalValue)) ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, program, host, moduleSpecifierResolutionHost, scriptPath, extensionOptions) : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, mode, program, host, moduleSpecifierResolutionHost, extensionOptions); } interface ExtensionOptions { readonly extensionsToSearch: readonly string[]; readonly referenceKind: ReferenceKind; readonly importingSourceFile: SourceFile; readonly endingPreference?: UserPreferences["importModuleSpecifierEnding"]; readonly resolutionMode?: ResolutionMode; } function getExtensionOptions(compilerOptions: CompilerOptions, referenceKind: ReferenceKind, importingSourceFile: SourceFile, typeChecker?: TypeChecker, preferences?: UserPreferences, resolutionMode?: ResolutionMode): ExtensionOptions { return { extensionsToSearch: flatten(getSupportedExtensionsForModuleResolution(compilerOptions, typeChecker)), referenceKind, importingSourceFile, endingPreference: preferences?.importModuleSpecifierEnding, resolutionMode, }; } function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, scriptPath: Path, extensionOptions: ExtensionOptions) { const compilerOptions = program.getCompilerOptions(); if (compilerOptions.rootDirs) { return getCompletionEntriesForDirectoryFragmentWithRootDirs( compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, program, host, moduleSpecifierResolutionHost, scriptPath, ); } else { return arrayFrom(getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, program, host, moduleSpecifierResolutionHost, /*moduleSpecifierIsRelative*/ true, scriptPath).values()); } } function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions, typeChecker?: TypeChecker): readonly string[][] { /** file extensions from ambient modules declarations e.g. *.css */ const ambientModulesExtensions = !typeChecker ? [] : mapDefined(typeChecker.getAmbientModules(), module => { const name = module.name.slice(1, -1); if (!name.startsWith("*.") || name.includes("/")) return; return name.slice(1); }); const extensions = [...getSupportedExtensions(compilerOptions), ambientModulesExtensions]; const moduleResolution = getEmitModuleResolutionKind(compilerOptions); return moduleResolutionUsesNodeModules(moduleResolution) ? getSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, extensions) : extensions; } /** * Takes a script path and returns paths for all potential folders that could be merged with its * containing folder via the "rootDirs" compiler option */ function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptDirectory: string, ignoreCase: boolean): readonly string[] { // Make all paths absolute/normalized if they are not already rootDirs = rootDirs.map(rootDirectory => ensureTrailingDirectorySeparator(normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory)))); // Determine the path to the directory containing the script relative to the root directory it is contained within const relativeDirectory = firstDefined(rootDirs, rootDirectory => containsPath(rootDirectory, scriptDirectory, basePath, ignoreCase) ? scriptDirectory.substr(rootDirectory.length) : undefined)!; // TODO: GH#18217 // Now find a path for each potential directory that is to be merged with the one containing the script return deduplicate<string>( [...rootDirs.map(rootDirectory => combinePaths(rootDirectory, relativeDirectory)), scriptDirectory].map(baseDir => removeTrailingDirectorySeparator(baseDir)), equateStringsCaseSensitive, compareStringsCaseSensitive, ); } function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, exclude: string): readonly NameAndKind[] { const compilerOptions = program.getCompilerOptions(); const basePath = compilerOptions.project || host.getCurrentDirectory(); const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase); return deduplicate<NameAndKind>( flatMap(baseDirectories, baseDirectory => arrayFrom(getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, program, host, moduleSpecifierResolutionHost, /*moduleSpecifierIsRelative*/ true, exclude).values())), (itemA, itemB) => itemA.name === itemB.name && itemA.kind === itemB.kind && itemA.extension === itemB.extension, ); } const enum ReferenceKind { Filename, ModuleSpecifier, } /** * 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, scriptDirectory: string, extensionOptions: ExtensionOptions, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, moduleSpecifierIsRelative: boolean, exclude?: string, result = createNameAndKindSet(), ): NameAndKindSet { if (fragment === undefined) { fragment = ""; } fragment = normalizeSlashes(fragment); /** * Remove the basename from the path. Note that we don't use the basename to filter completions; * the client is responsible for refining completions. */ if (!hasTrailingDirectorySeparator(fragment)) { fragment = getDirectoryPath(fragment); } if (fragment === "") { fragment = "." + directorySeparator; } fragment = ensureTrailingDirectorySeparator(fragment); const absolutePath = resolvePath(scriptDirectory, fragment); const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath); if (!moduleSpecifierIsRelative) { // check for a version redirect const packageJsonPath = findPackageJson(baseDirectory, host); if (packageJsonPath) { const packageJson = readJson(packageJsonPath, host as { readFile: (filename: string) => string | undefined; }); const typesVersions = (packageJson as any).typesVersions; if (typeof typesVersions === "object") { const versionPaths = getPackageJsonTypesVersionsPaths(typesVersions)?.paths; if (versionPaths) { const packageDirectory = getDirectoryPath(packageJsonPath); const pathInPackage = absolutePath.slice(ensureTrailingDirectorySeparator(packageDirectory).length); if (addCompletionEntriesFromPaths(result, pathInPackage, packageDirectory, extensionOptions, program, host, moduleSpecifierResolutionHost, versionPaths)) { // A true result means one of the `versionPaths` was matched, which will block relative resolution // to files and folders from here. All reachable paths given the pattern match are already added. return result; } } } } } const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); if (!tryDirectoryExists(host, baseDirectory)) return result; // Enumerate the available files if possible const files = tryReadDirectory(host, baseDirectory, extensionOptions.extensionsToSearch, /*exclude*/ undefined, /*include*/ ["./*"]); if (files) { for (let filePath of files) { filePath = normalizePath(filePath); if (exclude && comparePaths(filePath, exclude, scriptDirectory, ignoreCase) === Comparison.EqualTo) { continue; } const { name, extension } = getFilenameWithExtensionOption(getBaseFileName(filePath), program, extensionOptions, /*isExportsOrImportsWildcard*/ false); result.add(nameAndKind(name, ScriptElementKind.scriptElement, extension)); } } // If possible, get folder completion as well const directories = tryGetDirectories(host, baseDirectory); if (directories) { for (const directory of directories) { const directoryName = getBaseFileName(normalizePath(directory)); if (directoryName !== "@types") { result.add(directoryResult(directoryName)); } } } return result; } function getFilenameWithExtensionOption(name: string, program: Program, extensionOptions: ExtensionOptions, isExportsOrImportsWildcard: boolean): { name: string; extension: Extension | undefined; } { const nonJsResult = moduleSpecifiers.tryGetRealFileNameForNonJsDeclarationFileName(name); if (nonJsResult) { return { name: nonJsResult, extension: tryGetExtensionFromPath(nonJsResult) }; } if (extensionOptions.referenceKind === ReferenceKind.Filename) { return { name, extension: tryGetExtensionFromPath(name) }; } let allowedEndings = moduleSpecifiers.getModuleSpecifierPreferences( { importModuleSpecifierEnding: extensionOptions.endingPreference }, program, program.getCompilerOptions(), extensionOptions.importingSourceFile, ).getAllowedEndingsInPreferredOrder(extensionOptions.resolutionMode); if (isExportsOrImportsWildcard) { // If we're completing `import {} from "foo/|"` and subpaths are available via `"exports": { "./*": "./src/*" }`, // the completion must be a (potentially extension-swapped) file name. Dropping extensions and index files is not allowed. allowedEndings = allowedEndings.filter(e => e !== ModuleSpecifierEnding.Minimal && e !== ModuleSpecifierEnding.Index); } if (allowedEndings[0] === ModuleSpecifierEnding.TsExtension) { if (fileExtensionIsOneOf(name, supportedTSImplementationExtensions)) { return { name, extension: tryGetExtensionFromPath(name) }; } const outputExtension = moduleSpecifiers.tryGetJSExtensionForFile(name, program.getCompilerOptions()); return outputExtension ? { name: changeExtension(name, outputExtension), extension: outputExtension } : { name, extension: tryGetExtensionFromPath(name) }; } if ( !isExportsOrImportsWildcard && (allowedEndings[0] === ModuleSpecifierEnding.Minimal || allowedEndings[0] === ModuleSpecifierEnding.Index) && fileExtensionIsOneOf(name, [Extension.Js, Extension.Jsx, Extension.Ts, Extension.Tsx, Extension.Dts]) ) { return { name: removeFileExtension(name), extension: tryGetExtensionFromPath(name) }; } const outputExtension = moduleSpecifiers.tryGetJSExtensionForFile(name, program.getCompilerOptions()); return outputExtension ? { name: changeExtension(name, outputExtension), extension: outputExtension } : { name, extension: tryGetExtensionFromPath(name) }; } /** @returns whether `fragment` was a match for any `paths` (which should indicate whether any other path completions should be offered) */ function addCompletionEntriesFromPaths( result: NameAndKindSet, fragment: string, baseDirectory: string, extensionOptions: ExtensionOptions, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, paths: MapLike<string[]>, ) { const getPatternsForKey = (key: string) => paths[key]; const comparePaths = (a: string, b: string): Comparison => { const patternA = tryParsePattern(a); const patternB = tryParsePattern(b); const lengthA = typeof patternA === "object" ? patternA.prefix.length : a.length; const lengthB = typeof patternB === "object" ? patternB.prefix.length : b.length; return compareValues(lengthB, lengthA); }; return addCompletionEntriesFromPathsOrExportsOrImports(result, /*isExports*/ false, /*isImports*/ false, fragment, baseDirectory, extensionOptions, program, host, moduleSpecifierResolutionHost, getOwnKeys(paths), getPatternsForKey, comparePaths); } /** @returns whether `fragment` was a match for any `paths` (which should indicate whether any other path completions should be offered) */ function addCompletionEntriesFromPathsOrExportsOrImports( result: NameAndKindSet, isExports: boolean, isImports: boolean, fragment: string, baseDirectory: string, extensionOptions: ExtensionOptions, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, keys: readonly string[], getPatternsForKey: (key: string) => string[] | undefined, comparePaths: (a: string, b: string) => Comparison, ) { let pathResults: { results: NameAndKind[]; matchedPattern: boolean; }[] = []; let matchedPath: string | undefined; for (const key of keys) { if (key === ".") continue; const keyWithoutLeadingDotSlash = key .replace(/^\.\//, "") // remove leading "./" + ((isExports || isImports) && endsWith(key, "/") ? "*" : ""); // normalize trailing `/` to `/*` const patterns = getPatternsForKey(key); if (patterns) { const pathPattern = tryParsePattern(keyWithoutLeadingDotSlash); if (!pathPattern) continue; const isMatch = typeof pathPattern === "object" && isPatternMatch(pathPattern, fragment); const isLongestMatch = isMatch && (matchedPath === undefined || comparePaths(keyWithoutLeadingDotSlash, matchedPath) === Comparison.LessThan); if (isLongestMatch) { // If this is a higher priority match than anything we've seen so far, previous results from matches are invalid, e.g. // for `import {} from "some-package/|"` with a typesVersions: // { // "bar/*": ["bar/*"], // <-- 1. We add 'bar', but 'bar/*' doesn't match yet. // "*": ["dist/*"], // <-- 2. We match here and add files from dist. 'bar' is still ok because it didn't come from a match. // "foo/*": ["foo/*"] // <-- 3. We matched '*' earlier and added results from dist, but if 'foo/*' also matched, // } results in dist would not be visible. 'bar' still stands because it didn't come from a match. // This is especially important if `dist/foo` is a folder, because if we fail to clear results // added by the '*' match, after typing `"some-package/foo/|"` we would get file results from both // ./dist/foo and ./foo, when only the latter will actually be resolvable. // See pathCompletionsTypesVersionsWildcard6.ts. matchedPath = keyWithoutLeadingDotSlash; pathResults = pathResults.filter(r => !r.matchedPattern); } if (typeof pathPattern === "string" || matchedPath === undefined || comparePaths(keyWithoutLeadingDotSlash, matchedPath) !== Comparison.GreaterThan) { pathResults.push({ matchedPattern: isMatch, results: getCompletionsForPathMapping(keyWithoutLeadingDotSlash, patterns, fragment, baseDirectory, extensionOptions, isExports, isImports, program, host, moduleSpecifierResolutionHost) .map(({ name, kind, extension }) => nameAndKind(name, kind, extension)), }); } } } pathResults.forEach(pathResult => pathResult.results.forEach(r => result.add(r))); return matchedPath !== undefined; } /** * Check all of the declared modules and those in node modules. Possible sources of modules: * Modules that are found by the type checker * Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option) * 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, mode: ResolutionMode, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, extensionOptions: ExtensionOptions, ): readonly NameAndKind[] { const typeChecker = program.getTypeChecker(); const compilerOptions = program.getCompilerOptions(); const { baseUrl, paths } = compilerOptions; const result = createNameAndKindSet(); const moduleResolution = getEmitModuleResolutionKind(compilerOptions); if (baseUrl) { const absolute = normalizePath(combinePaths(host.getCurrentDirectory(), baseUrl)); getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, program, host, moduleSpecifierResolutionHost, /*moduleSpecifierIsRelative*/ false, /*exclude*/ undefined, result); } if (paths) { const absolute = getPathsBasePath(compilerOptions, host)!; addCompletionEntriesFromPaths(result, fragment, absolute, extensionOptions, program, host, moduleSpecifierResolutionHost, paths); } const fragmentDirectory = getFragmentDirectory(fragment); for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) { result.add(nameAndKind(ambientName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); } getCompletionEntriesFromTypings(program, host, moduleSpecifierResolutionHost, scriptPath, fragmentDirectory, extensionOptions, result); if (moduleResolutionUsesNodeModules(moduleResolution)) { // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies. // (But do if we didn't find anything, e.g. 'package.json' missing.) let foundGlobal = false; if (fragmentDirectory === undefined) { for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) { const moduleResult = nameAndKind(moduleName, ScriptElementKind.externalModuleName, /*extension*/ undefined); if (!result.has(moduleResult.name)) { foundGlobal = true; result.add(moduleResult); } } } if (!foundGlobal) { const resolvePackageJsonExports = getResolvePackageJsonExports(compilerOptions); const resolvePackageJsonImports = getResolvePackageJsonImports(compilerOptions); let seenPackageScope = false; const importsLookup = (directory: string) => { if (resolvePackageJsonImports && !seenPackageScope) { const packageFile = combinePaths(directory, "package.json"); if (seenPackageScope = tryFileExists(host, packageFile)) { const packageJson = readJson(packageFile, host); exportsOrImportsLookup((packageJson as MapLike<unknown>).imports, fragment, directory, /*isExports*/ false, /*isImports*/ true); } } }; let ancestorLookup: (directory: string) => void | undefined = ancestor => { const nodeModules = combinePaths(ancestor, "node_modules"); if (tryDirectoryExists(host, nodeModules)) { getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, program, host, moduleSpecifierResolutionHost, /*moduleSpecifierIsRelative*/ false, /*exclude*/ undefined, result); } importsLookup(ancestor); }; if (fragmentDirectory && resolvePackageJsonExports) { const nodeModulesDirectoryOrImportsLookup = ancestorLookup; ancestorLookup = ancestor => { const components = getPathComponents(fragment); components.shift(); // shift off empty root let packagePath = components.shift(); if (!packagePath) { return nodeModulesDirectoryOrImportsLookup(ancestor); } if (startsWith(packagePath, "@")) { const subName = components.shift(); if (!subName) { return nodeModulesDirectoryOrImportsLookup(ancestor); } packagePath = combinePaths(packagePath, subName); } if (resolvePackageJsonImports && startsWith(packagePath, "#")) { return importsLookup(ancestor); } const packageDirectory = combinePaths(ancestor, "node_modules", packagePath); const packageFile = combinePaths(packageDirectory, "package.json"); if (tryFileExists(host, packageFile)) { const packageJson = readJson(packageFile, host); const fragmentSubpath = components.join("/") + (components.length && hasTrailingDirectorySeparator(fragment) ? "/" : ""); if (exportsOrImportsLookup((packageJson as MapLike<unknown>).exports, fragmentSubpath, packageDirectory, /*isExports*/ true, /*isImports*/ false)) { return; } } return nodeModulesDirectoryOrImportsLookup(ancestor); }; } forEachAncestorDirectoryStoppingAtGlobalCache(host, scriptPath, ancestorLookup); } } return arrayFrom(result.values()); /** Returns true if the search should stop */ function exportsOrImportsLookup(lookupTable: unknown, fragment: string, baseDirectory: string, isExports: boolean, isImports: boolean): boolean { if (typeof lookupTable !== "object" || lookupTable === null) { // eslint-disable-line no-restricted-syntax return lookupTable !== undefined; // null lookupTable or entrypoint only } const keys = getOwnKeys(lookupTable as MapLike<unknown>); const conditions = getConditions(compilerOptions, mode); addCompletionEntriesFromPathsOrExportsOrImports( result, isExports, isImports, fragment, baseDirectory, extensionOptions, program, host, moduleSpecifierResolutionHost, keys, key => { const pattern = getPatternFromFirstMatchingCondition((lookupTable as MapLike<unknown>)[key], conditions); if (pattern === undefined) { return undefined; } return singleElementArray(endsWith(key, "/") && endsWith(pattern, "/") ? pattern + "*" : pattern); }, comparePatternKeys, ); return true; } } function getPatternFromFirstMatchingCondition(target: unknown, conditions: readonly string[]): string | undefined { if (typeof target === "string") { return target; } if (target && typeof target === "object" && !isArray(target)) { for (const condition in target) { if (condition === "default" || conditions.includes(condition) || isApplicableVersionedTypesKey(conditions, condition)) { const pattern = (target as MapLike<unknown>)[condition]; return getPatternFromFirstMatchingCondition(pattern, conditions); } } } } function getFragmentDirectory(fragment: string): string | undefined { return containsSlash(fragment) ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; } function getCompletionsForPathMapping( path: string, patterns: readonly string[], fragment: string, packageDirectory: string, extensionOptions: ExtensionOptions, isExports: boolean, isImports: boolean, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, ): readonly NameAndKind[] { const parsedPath = tryParsePattern(path); if (!parsedPath) { return emptyArray; } // no stars in the pattern if (typeof parsedPath === "string") { // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion. return justPathMappingName(path, ScriptElementKind.scriptElement); } const remainingFragment = tryRemovePrefix(fragment, parsedPath.prefix); if (remainingFragment === undefined) { const starIsFullPathComponent = endsWith(path, "/*"); return starIsFullPathComponent ? justPathMappingName(parsedPath.prefix, ScriptElementKind.directory) : flatMap(patterns, pattern => getModulesForPathsPattern("", packageDirectory, pattern, extensionOptions, isExports, isImports, program, host, moduleSpecifierResolutionHost)?.map(({ name, ...rest }) => ({ name: parsedPath.prefix + name + parsedPath.suffix, ...rest }))); } return flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, packageDirectory, pattern, extensionOptions, isExports, isImports, program, host, moduleSpecifierResolutionHost)); function justPathMappingName(name: string, kind: ScriptElementKind.directory | ScriptElementKind.scriptElement): readonly NameAndKind[] { return startsWith(name, fragment) ? [{ name: removeTrailingDirectorySeparator(name), kind, extension: undefined }] : emptyArray; } } function getModulesForPathsPattern( fragment: string, packageDirectory: string, pattern: string, extensionOptions: ExtensionOptions, isExports: boolean, isImports: boolean, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, ): readonly NameAndKind[] | undefined { if (!host.readDirectory) { return undefined; } const parsed = tryParsePattern(pattern); if (parsed === undefined || isString(parsed)) { return undefined; } // The prefix has two effective parts: the directory path and the base component after the filepath that is not a // full directory component. For example: directory/path/of/prefix/base* const normalizedPrefix = resolvePath(parsed.prefix); const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix); const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix); const fragmentHasPath = containsSlash(fragment); const fragmentDirectory = fragmentHasPath ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; const getCommonSourceDirectory = () => moduleSpecifierResolutionHost.getCommonSourceDirectory(); const ignoreCase = !hostUsesCaseSensitiveFileNames(moduleSpecifierResolutionHost); const outDir = program.getCompilerOptions().outDir; const declarationDir = program.getCompilerOptions().declarationDir; // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory; // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". const baseDirectory = normalizePath(combinePaths(packageDirectory, expandedPrefixDirectory)); const possibleInputBaseDirectoryForOutDir = isImports && outDir && getPossibleOriginalInputPathWithoutChangingExt(baseDirectory, ignoreCase, outDir, getCommonSourceDirectory); const possibleInputBaseDirectoryForDeclarationDir = isImports && declarationDir && getPossibleOriginalInputPathWithoutChangingExt(baseDirectory, ignoreCase, declarationDir, getCommonSourceDirectory); const normalizedSuffix = normalizePath(parsed.suffix); const declarationExtension = normalizedSuffix && getDeclarationEmitExtensionForPath("_" + normalizedSuffix); const inputExtension = normalizedSuffix ? getPossibleOriginalInputExtensionForExtension("_" + normalizedSuffix) : undefined; const matchingSuffixes = [ declarationExtension && changeExtension(normalizedSuffix, declarationExtension), ...(inputExtension ? inputExtension.map(ext => changeExtension(normalizedSuffix, ext)) : []), normalizedSuffix, ].filter(isString); // If we have a suffix, then we read the directory all the way down to avoid returning completions for // directories that don't contain files that would match the suffix. A previous comment here was concerned // about the case where `normalizedSuffix` includes a `?` character, which should be interpreted literally, // but will match any single character as part of the `include` pattern in `tryReadDirectory`. This is not // a problem, because (in the extremely unusual circumstance where the suffix has a `?` in it) a `?` // interpreted as "any character" can only return *too many* results as compared to the literal // interpretation, so we can filter those superfluous results out via `trimPrefixAndSuffix` as we've always // done. const includeGlobs = normalizedSuffix ? matchingSuffixes.map(suffix => "**/*" + suffix) : ["./*"]; const isExportsOrImportsWildcard = (isExports || isImports) && endsWith(pattern, "/*"); let matches = getMatchesWithPrefix(baseDirectory); if (possibleInputBaseDirectoryForOutDir) { matches = concatenate(matches, getMatchesWithPrefix(possibleInputBaseDirectoryForOutDir)); } if (possibleInputBaseDirectoryForDeclarationDir) { matches = concatenate(matches, getMatchesWithPrefix(possibleInputBaseDirectoryForDeclarationDir)); } // If we had a suffix, we already recursively searched for all possible files that could match // it and returned the directories leading to those files. Otherwise, assume any directory could // have something valid to import. if (!normalizedSuffix) { matches = concatenate(matches, getDirectoryMatches(baseDirectory)); if (possibleInputBaseDirectoryForOutDir) { matches = concatenate(matches, getDirectoryMatches(possibleInputBaseDirectoryForOutDir)); } if (possibleInputBaseDirectoryForDeclarationDir) { matches = concatenate(matches, getDirectoryMatches(possibleInputBaseDirectoryForDeclarationDir)); } } return matches; function getMatchesWithPrefix(directory: string) { const completePrefix = fragmentHasPath ? directory : ensureTrailingDirectorySeparator(directory) + normalizedPrefixBase; return mapDefined(tryReadDirectory(host, directory, extensionOptions.extensionsToSearch, /*exclude*/ undefined, includeGlobs), match => { const trimmedWithPattern = trimPrefixAndSuffix(match, completePrefix); if (trimmedWithPattern) { if (containsSlash(trimmedWithPattern)) { return directoryResult(getPathComponents(removeLeadingDirectorySeparator(trimmedWithPattern))[1]); } const { name, extension } = getFilenameWithExtensionOption(trimmedWithPattern, program, extensionOptions, isExportsOrImportsWildcard); return nameAndKind(name, ScriptElementKind.scriptElement, extension); } }); } function getDirectoryMatches(directoryName: string) { return mapDefined(tryGetDirectories(host, directoryName), dir => dir === "node_modules" ? undefined : directoryResult(dir)); } function trimPrefixAndSuffix(path: string, prefix: string): string | undefined { return firstDefined(matchingSuffixes, suffix => { const inner = withoutStartAndEnd(normalizePath(path), prefix, suffix); return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner); }); } } function withoutStartAndEnd(s: string, start: string, end: string): string | undefined { return startsWith(s, start) && endsWith(s, end) ? s.slice(start.length, s.length - end.length) : undefined; } function removeLeadingDirectorySeparator(path: string): string { return path[0] === directorySeparator ? path.slice(1) : path; } function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): readonly string[] { // Get modules that the type checker picked up const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name)); const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment) && !moduleName.includes("*")); // Nested modules of the form "module-name/sub" need to be adjusted to only return the string // after the last '/' that appears in the fragment because that's where the replacement span // starts if (fragmentDirectory !== undefined) { const moduleNameWithSeparator = ensureTrailingDirectorySeparator(fragmentDirectory); return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeparator)); } return nonRelativeModuleNames; } function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost): readonly PathCompletion[] | undefined { const compilerOptions = program.getCompilerOptions(); const token = getTokenAtPosition(sourceFile, position); const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos); const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end); if (!range) { return undefined; } const text = sourceFile.text.slice(range.pos, position); const match = tripleSlashDirectiveFragmentRegex.exec(text); if (!match) { return undefined; } const [, prefix, kind, toComplete] = match; const scriptPath = getDirectoryPath(sourceFile.path); const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, ReferenceKind.Filename, sourceFile), program, host, moduleSpecifierResolutionHost, /*moduleSpecifierIsRelative*/ true, sourceFile.path) : kind === "types" ? getCompletionEntriesFromTypings(program, host, moduleSpecifierResolutionHost, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions, ReferenceKind.ModuleSpecifier, sourceFile)) : Debug.fail(); return addReplacementSpans(toComplete, range.pos + prefix.length, arrayFrom(names.values())); } function getCompletionEntriesFromTypings(program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, scriptPath: string, fragmentDirectory: string | undefined, extensionOptions: ExtensionOptions, result = createNameAndKindSet()): NameAndKindSet { const options = program.getCompilerOptions(); // Check for typings specified in compiler options const seen = new Map<string, true>(); const typeRoots = tryAndIgnoreErrors(() => getEffectiveTypeRoots(options, host)) || emptyArray; for (const root of typeRoots) { getCompletionEntriesFromDirectories(root); } // Also get all @types typings installed in visible node_modules directories for (const packageJson of findPackageJsons(scriptPath, host)) { const typesDir = combinePaths(getDirectoryPath(packageJson), "node_modules/@types"); getCompletionEntriesFromDirectories(typesDir); } return result; function getCompletionEntriesFromDirectories(directory: string): void { if (!tryDirectoryExists(host, directory)) return; for (const typeDirectoryName of tryGetDirectories(host, directory)) { const packageName = unmangleScopedPackageName(typeDirectoryName); if (options.types && !contains(options.types, packageName)) continue; if (fragmentDirectory === undefined) { if (!seen.has(packageName)) { result.add(nameAndKind(packageName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); seen.set(packageName, true); } } else { const baseDirectory = combinePaths(directory, typeDirectoryName); const remainingFragment = tryRemoveDirectoryPrefix(fragmentDirectory, packageName, hostGetCanonicalFileName(host)); if (remainingFragment !== undefined) { getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, program, host, moduleSpecifierResolutionHost, /*moduleSpecifierIsRelative*/ false, /*exclude*/ undefined, result); } } } } } function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): readonly string[] { if (!host.readFile || !host.fileExists) return emptyArray; const result: string[] = []; for (const packageJson of findPackageJsons(scriptPath, host)) { const contents = readJson(packageJson, host as { readFile: (filename: string) => string | undefined; }); // Cast to assert that readFile is defined // Provide completions for all non @types dependencies for (const key of nodeModulesDependencyKeys) { const dependencies: object | undefined = (contents as any)[key]; if (!dependencies) continue; for (const dep in dependencies) { if (hasProperty(dependencies, dep) && !startsWith(dep, "@types/")) { result.push(dep); } } } } return result; } // Replace everything after the last directory separator that appears function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan | undefined { const index = Math.max(text.lastIndexOf(directorySeparator), text.lastIndexOf(altDirectorySeparator)); const offset = index !== -1 ? index + 1 : 0; // If the range is an identifier, span is unnecessary. const length = text.length - offset; return length === 0 || isIdentifierText(text.substr(offset, length), ScriptTarget.ESNext) ? undefined : createTextSpan(textStart + offset, length); } // Returns true if the path is explicitly relative to the script (i.e. relative to . or ..) function isPathRelativeToScript(path: string) { if (path && path.length >= 2 && path.charCodeAt(0) === CharacterCodes.dot) { const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1; const slashCharCode = path.charCodeAt(slashIndex); return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash; } return false; } /** * Matches a triple slash reference directive with an incomplete string literal for its path. Used * to determine if the caret is currently within the string literal and capture the literal fragment * for completions. * For example, this matches * * /// <reference path="fragment * * but not * * /// <reference path="fragment" */ const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\x03"]*)$/; const nodeModulesDependencyKeys: readonly string[] = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]; function containsSlash(fragment: string) { return fragment.includes(directorySeparator); } /** * Matches * require("" * require("") */ function isRequireCallArgument(node: Node) { return isCallExpression(node.parent) && firstOrUndefined(node.parent.arguments) === node && isIdentifier(node.parent.expression) && node.parent.expression.escapedText === "require"; }