diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a7be7633c35a9..93a0f364f05ba 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1900,6 +1900,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { getMemberOverrideModifierStatus, isTypeParameterPossiblyReferenced, typeHasCallOrConstructSignatures, + getSymbolFlags, }; function getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e83005b3201a2..f0e075ba5af73 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5361,6 +5361,7 @@ export interface TypeChecker { /** @internal */ getMemberOverrideModifierStatus(node: ClassLikeDeclaration, member: ClassElement, memberSymbol: Symbol): MemberOverrideStatus; /** @internal */ isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node): boolean; /** @internal */ typeHasCallOrConstructSignatures(type: Type): boolean; + /** @internal */ getSymbolFlags(symbol: Symbol): SymbolFlags; } /** @internal */ diff --git a/src/services/codefixes/convertToEsModule.ts b/src/services/codefixes/convertToEsModule.ts index 9b725b591c6a3..50860aacf0355 100644 --- a/src/services/codefixes/convertToEsModule.ts +++ b/src/services/codefixes/convertToEsModule.ts @@ -1,6 +1,5 @@ import { createCodeFixActionWithoutFixAll, - moduleSpecifierToValidIdentifier, registerCodeFix, } from "../_namespaces/ts.codefix.js"; import { @@ -59,6 +58,7 @@ import { mapIterator, MethodDeclaration, Modifier, + moduleSpecifierToValidIdentifier, Node, NodeArray, NodeFlags, diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index e528701942ce9..e0e99701d9361 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -41,12 +41,12 @@ import { flatMap, flatMapIterator, forEachExternalModuleToImportFrom, + forEachNameOfDefaultExport, formatting, FutureSourceFile, FutureSymbolExportInfo, getAllowSyntheticDefaultImports, getBaseFileName, - getDefaultExportInfoWorker, getDefaultLikeExportInfo, getDirectoryPath, getEmitModuleFormatOfFileWorker, @@ -55,7 +55,6 @@ import { getEmitScriptTarget, getExportInfoMap, getImpliedNodeFormatForEmitWorker, - getMeaningFromDeclaration, getMeaningFromLocation, getNameForExportedSymbol, getOutputExtension, @@ -71,6 +70,7 @@ import { hasJSFileExtension, hostGetCanonicalFileName, Identifier, + identity, ImportClause, ImportEqualsDeclaration, importFromModuleSpecifier, @@ -81,8 +81,6 @@ import { isExternalModuleReference, isFullSourceFile, isIdentifier, - isIdentifierPart, - isIdentifierStart, isImportableFile, isImportDeclaration, isImportEqualsDeclaration, @@ -96,7 +94,6 @@ import { isNamespaceImport, isRequireVariableStatement, isSourceFileJS, - isStringANonContextualKeyword, isStringLiteral, isStringLiteralLike, isTypeOnlyImportDeclaration, @@ -114,6 +111,7 @@ import { ModuleKind, moduleResolutionUsesNodeModules, moduleSpecifiers, + moduleSymbolToValidIdentifier, MultiMap, Mutable, NamedImports, @@ -129,12 +127,9 @@ import { pathIsBareSpecifier, Program, QuotePreference, - removeFileExtension, - removeSuffix, RequireOrImportCall, RequireVariableStatement, sameMap, - ScriptTarget, SemanticMeaning, shouldUseUriStyleNodeCoreModules, single, @@ -873,7 +868,6 @@ function getAllExportInfoForSymbol(importingFile: SourceFile | FutureSourceFile, } function getSingleExportInfoForSymbol(symbol: Symbol, symbolName: string, moduleSymbol: Symbol, program: Program, host: LanguageServiceHost): SymbolExportInfo { - const compilerOptions = program.getCompilerOptions(); const mainProgramInfo = getInfoWithChecker(program.getTypeChecker(), /*isFromPackageJson*/ false); if (mainProgramInfo) { return mainProgramInfo; @@ -882,7 +876,7 @@ function getSingleExportInfoForSymbol(symbol: Symbol, symbolName: string, module return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider, /*isFromPackageJson*/ true), `Could not find symbol in specified module for code actions`); function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined { - const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker); if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) { return { symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: undefined, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(symbol, checker).flags, isFromPackageJson }; } @@ -1194,7 +1188,7 @@ function getNewImportFixes( const exportEquals = checker.resolveExternalModuleSymbol(exportInfo.moduleSymbol); let namespacePrefix; if (exportEquals !== exportInfo.moduleSymbol) { - namespacePrefix = getDefaultExportInfoWorker(exportEquals, checker, compilerOptions)?.name; + namespacePrefix = forEachNameOfDefaultExport(exportEquals, checker, compilerOptions, /*preferCapitalizedNames*/ false, identity)!; } namespacePrefix ||= moduleSymbolToValidIdentifier( exportInfo.moduleSymbol, @@ -1533,14 +1527,18 @@ function getExportInfos( cancellationToken.throwIfCancellationRequested(); const compilerOptions = program.getCompilerOptions(); - const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); - if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === symbolName) && symbolHasMeaning(defaultInfo.resolvedSymbol, currentTokenMeaning)) { + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker); + if ( + defaultInfo + && symbolFlagsHaveMeaning(checker.getSymbolFlags(defaultInfo.symbol), currentTokenMeaning) + && forEachNameOfDefaultExport(defaultInfo.symbol, checker, compilerOptions, isJsxTagName, name => name === symbolName) + ) { addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson); } // check exports with the same name const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); - if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { + if (exportSymbolWithIdenticalName && symbolFlagsHaveMeaning(checker.getSymbolFlags(exportSymbolWithIdenticalName), currentTokenMeaning)) { addSymbol(moduleSymbol, sourceFile, exportSymbolWithIdenticalName, ExportKind.Named, program, isFromPackageJson); } }); @@ -2009,44 +2007,12 @@ function createConstEqualsRequireDeclaration(name: string | ObjectBindingPattern ) as RequireVariableStatement; } -function symbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean { - return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)); -} - -/** @internal */ -export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined, forceCapitalize: boolean): string { - return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target, forceCapitalize); -} - -/** @internal */ -export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget | undefined, forceCapitalize?: boolean): string { - const baseName = getBaseFileName(removeSuffix(moduleSpecifier, "/index")); - let res = ""; - let lastCharWasValid = true; - const firstCharCode = baseName.charCodeAt(0); - if (isIdentifierStart(firstCharCode, target)) { - res += String.fromCharCode(firstCharCode); - if (forceCapitalize) { - res = res.toUpperCase(); - } - } - else { - lastCharWasValid = false; - } - for (let i = 1; i < baseName.length; i++) { - const ch = baseName.charCodeAt(i); - const isValid = isIdentifierPart(ch, target); - if (isValid) { - let char = String.fromCharCode(ch); - if (!lastCharWasValid) { - char = char.toUpperCase(); - } - res += char; - } - lastCharWasValid = isValid; - } - // Need `|| "_"` to ensure result isn't empty. - return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`; +function symbolFlagsHaveMeaning(flags: SymbolFlags, meaning: SemanticMeaning): boolean { + return meaning === SemanticMeaning.All ? true : + meaning & SemanticMeaning.Value ? !!(flags & SymbolFlags.Value) : + meaning & SemanticMeaning.Type ? !!(flags & SymbolFlags.Type) : + meaning & SemanticMeaning.Namespace ? !!(flags & SymbolFlags.Namespace) : + false; } function getImpliedNodeFormatForEmit(file: SourceFile | FutureSourceFile, program: Program) { diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index e75303efcc819..24d0c7b9b8000 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -1,6 +1,7 @@ import { __String, addToSeen, + append, arrayIsEqualTo, CancellationToken, CompilerOptions, @@ -10,15 +11,15 @@ import { emptyArray, ensureTrailingDirectorySeparator, findIndex, - firstDefined, forEachAncestorDirectory, forEachEntry, FutureSourceFile, getBaseFileName, GetCanonicalFileName, + getDefaultLikeExportNameFromDeclaration, getDirectoryPath, + getEmitScriptTarget, getLocalSymbolForExportDefault, - getNameForExportedSymbol, getNamesForExportedSymbol, getNodeModulePathParts, getPackageNameFromTypesPackageName, @@ -28,12 +29,9 @@ import { hostGetCanonicalFileName, hostUsesCaseSensitiveFileNames, InternalSymbolName, - isExportAssignment, - isExportSpecifier, isExternalModuleNameRelative, isExternalModuleSymbol, isExternalOrCommonJsModule, - isIdentifier, isKnownSymbol, isNonGlobalAmbientModule, isPrivateIdentifierSymbol, @@ -42,13 +40,13 @@ import { ModuleSpecifierCache, ModuleSpecifierResolutionHost, moduleSpecifiers, + moduleSymbolToValidIdentifier, nodeModulesPathPart, PackageJsonImportFilter, Path, pathContainsNodeModules, Program, skipAlias, - skipOuterExpressions, SourceFile, startsWith, Statement, @@ -56,7 +54,6 @@ import { Symbol, SymbolFlags, timestamp, - tryCast, TypeChecker, unescapeLeadingUnderscores, unmangleScopedPackageName, @@ -502,14 +499,13 @@ export function getExportInfoMap(importingFile: SourceFile | FutureSourceFile, h } host.log?.("getExportInfoMap: cache miss or empty; calculating new results"); - const compilerOptions = program.getCompilerOptions(); let moduleCount = 0; try { forEachExternalModuleToImportFrom(program, host, preferences, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => { if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested(); const seenExports = new Map<__String, true>(); const checker = program.getTypeChecker(); - const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker); // Note: I think we shouldn't actually see resolved module symbols here, but weird merges // can cause it to happen: see 'completionsImport_mergedReExport.ts' if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) { @@ -551,61 +547,49 @@ export function getExportInfoMap(importingFile: SourceFile | FutureSourceFile, h } /** @internal */ -export function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) { - const exported = getDefaultLikeExportWorker(moduleSymbol, checker); - if (!exported) return undefined; - const { symbol, exportKind } = exported; - const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions); - return info && { symbol, exportKind, ...info }; +export function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker) { + const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; + const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol); + if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default }; } function isImportableSymbol(symbol: Symbol, checker: TypeChecker) { return !checker.isUndefinedSymbol(symbol) && !checker.isUnknownSymbol(symbol) && !isKnownSymbol(symbol) && !isPrivateIdentifierSymbol(symbol); } -function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol; readonly exportKind: ExportKind; } | undefined { - const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); - if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; - const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol); - if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default }; -} +/** + * @internal + * May call `cb` multiple times with the same name. + * Terminates when `cb` returns a truthy value. + */ +export function forEachNameOfDefaultExport(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, preferCapitalizedNames: boolean, cb: (name: string) => T | undefined): T | undefined { + let chain: Symbol[] | undefined; + let current: Symbol | undefined = defaultExport; + + while (current) { + // The predecessor to this function also looked for a name on the `localSymbol` + // of default exports, but I think `getDefaultLikeExportNameFromDeclaration` + // accomplishes the same thing via syntax - no tests failed when I removed it. + const fromDeclaration = getDefaultLikeExportNameFromDeclaration(current); + if (fromDeclaration) { + const final = cb(fromDeclaration); + if (final) return final; + } -/** @internal */ -export function getDefaultExportInfoWorker(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly resolvedSymbol: Symbol; readonly name: string; } | undefined { - const localSymbol = getLocalSymbolForExportDefault(defaultExport); - if (localSymbol) return { resolvedSymbol: localSymbol, name: localSymbol.name }; - - const name = getNameForExportDefault(defaultExport); - if (name !== undefined) return { resolvedSymbol: defaultExport, name }; - - if (defaultExport.flags & SymbolFlags.Alias) { - const aliased = checker.getImmediateAliasedSymbol(defaultExport); - if (aliased && aliased.parent) { - // - `aliased` will be undefined if the module is exporting an unresolvable name, - // but we can still offer completions for it. - // - `aliased.parent` will be undefined if the module is exporting `globalThis.something`, - // or another expression that resolves to a global. - return getDefaultExportInfoWorker(aliased, checker, compilerOptions); + if (current.escapedName !== InternalSymbolName.Default && current.escapedName !== InternalSymbolName.ExportEquals) { + const final = cb(current.name); + if (final) return final; } - } - if ( - defaultExport.escapedName !== InternalSymbolName.Default && - defaultExport.escapedName !== InternalSymbolName.ExportEquals - ) { - return { resolvedSymbol: defaultExport, name: defaultExport.getName() }; + chain = append(chain, current); + current = current.flags & SymbolFlags.Alias ? checker.getImmediateAliasedSymbol(current) : undefined; } - return { resolvedSymbol: defaultExport, name: getNameForExportedSymbol(defaultExport, compilerOptions.target) }; -} -function getNameForExportDefault(symbol: Symbol): string | undefined { - return symbol.declarations && firstDefined(symbol.declarations, declaration => { - if (isExportAssignment(declaration)) { - return tryCast(skipOuterExpressions(declaration.expression), isIdentifier)?.text; - } - else if (isExportSpecifier(declaration)) { - Debug.assert(declaration.name.text === InternalSymbolName.Default, "Expected the specifier to be a default export"); - return declaration.propertyName && declaration.propertyName.text; + for (const symbol of chain ?? emptyArray) { + if (symbol.parent && isExternalModuleSymbol(symbol.parent)) { + const final = cb(moduleSymbolToValidIdentifier(symbol.parent, getEmitScriptTarget(compilerOptions), preferCapitalizedNames)); + if (final) return final; } - }); + } } diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index 5614efbca075f..55e14b576e1f7 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -1,7 +1,6 @@ import { ApplicableRefactorInfo, arrayFrom, - codefix, Debug, Diagnostics, emptyArray, @@ -29,6 +28,7 @@ import { isPropertyAccessOrQualifiedName, isShorthandPropertyAssignment, isStringLiteral, + moduleSpecifierToValidIdentifier, NamedImports, NamespaceImport, or, @@ -222,7 +222,7 @@ export function doChangeNamedToNamespaceOrDefault(sourceFile: SourceFile, progra toConvertSymbols.add(symbol); } }); - const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module"; + const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module"; function hasNamespaceNameConflict(namedImport: ImportSpecifier): boolean { // We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict. // A name conflict means that, in a scope where we would like to use the preferred namespace name, there already exists a symbol with that name in that scope. diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 38e6b137bad1f..613f8faf9b5ed 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -118,6 +118,7 @@ import { ModifierLike, ModuleDeclaration, ModuleKind, + moduleSpecifierToValidIdentifier, NamedImportBindings, Node, NodeFlags, @@ -392,7 +393,7 @@ function updateNamespaceLikeImport( oldImportNode: SupportedImport, quotePreference: QuotePreference, ): void { - const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleSpecifier, ScriptTarget.ESNext); + const preferredNewNamespaceName = moduleSpecifierToValidIdentifier(newModuleSpecifier, ScriptTarget.ESNext); let needUniqueName = false; const toChange: Identifier[] = []; FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index a5f068eef754c..5a9048dc0c114 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -20,7 +20,6 @@ import { ClassDeclaration, ClassExpression, clone, - codefix, combinePaths, CommentKind, CommentRange, @@ -92,6 +91,7 @@ import { FunctionLikeDeclaration, FutureSourceFile, getAssignmentDeclarationKind, + getBaseFileName, getCombinedNodeFlagsAlwaysIncludeJSDoc, getDirectoryPath, getEmitModuleKind, @@ -181,6 +181,8 @@ import { isGlobalScopeAugmentation, isHeritageClause, isIdentifier, + isIdentifierPart, + isIdentifierStart, isImportCall, isImportClause, isImportDeclaration, @@ -240,6 +242,7 @@ import { isSetAccessorDeclaration, isSourceFile, isSourceFileJS, + isStringANonContextualKeyword, isStringDoubleQuoted, isStringLiteral, isStringLiteralLike, @@ -319,6 +322,8 @@ import { pseudoBigIntToString, QualifiedName, RefactorContext, + removeFileExtension, + removeSuffix, Scanner, ScriptElementKind, ScriptElementKindModifier, @@ -4023,8 +4028,8 @@ export function getNamesForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTa if (needsNameFromDeclaration(symbol)) { const fromDeclaration = getDefaultLikeExportNameFromDeclaration(symbol); if (fromDeclaration) return fromDeclaration; - const fileNameCase = codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*forceCapitalize*/ false); - const capitalized = codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*forceCapitalize*/ true); + const fileNameCase = moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*forceCapitalize*/ false); + const capitalized = moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*forceCapitalize*/ true); if (fileNameCase === capitalized) return fileNameCase; return [fileNameCase, capitalized]; } @@ -4039,7 +4044,7 @@ export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTar // - export { foo as default } => foo // - export default 0 => filename converted to camelCase return getDefaultLikeExportNameFromDeclaration(symbol) - || codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, !!preferCapitalized); + || moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, !!preferCapitalized); } return symbol.name; } @@ -4048,7 +4053,8 @@ function needsNameFromDeclaration(symbol: Symbol) { return !(symbol.flags & SymbolFlags.Transient) && (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default); } -function getDefaultLikeExportNameFromDeclaration(symbol: Symbol): string | undefined { +/** @internal */ +export function getDefaultLikeExportNameFromDeclaration(symbol: Symbol): string | undefined { return firstDefined(symbol.declarations, d => { // "export default" in this case. See `ExportAssignment`for more details. if (isExportAssignment(d)) { @@ -4063,7 +4069,8 @@ function getDefaultLikeExportNameFromDeclaration(symbol: Symbol): string | undef }); } -function getSymbolParentOrFail(symbol: Symbol) { +/** @internal */ +export function getSymbolParentOrFail(symbol: Symbol) { return Debug.checkDefined( symbol.parent, `Symbol parent was undefined. Flags: ${Debug.formatSymbolFlags(symbol.flags)}. ` + @@ -4078,6 +4085,42 @@ function getSymbolParentOrFail(symbol: Symbol) { ); } +/** @internal */ +export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined, forceCapitalize: boolean): string { + return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target, forceCapitalize); +} + +/** @internal */ +export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget | undefined, forceCapitalize?: boolean): string { + const baseName = getBaseFileName(removeSuffix(moduleSpecifier, "/index")); + let res = ""; + let lastCharWasValid = true; + const firstCharCode = baseName.charCodeAt(0); + if (isIdentifierStart(firstCharCode, target)) { + res += String.fromCharCode(firstCharCode); + if (forceCapitalize) { + res = res.toUpperCase(); + } + } + else { + lastCharWasValid = false; + } + for (let i = 1; i < baseName.length; i++) { + const ch = baseName.charCodeAt(i); + const isValid = isIdentifierPart(ch, target); + if (isValid) { + let char = String.fromCharCode(ch); + if (!lastCharWasValid) { + char = char.toUpperCase(); + } + res += char; + } + lastCharWasValid = isValid; + } + // Need `|| "_"` to ensure result isn't empty. + return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`; +} + /** * Useful to check whether a string contains another string at a specific index * without allocating another string or traversing the entire contents of the outer string. diff --git a/tests/cases/fourslash/completionsImportDefaultExportCrash.ts b/tests/cases/fourslash/completionsImportDefaultExportCrash.ts new file mode 100644 index 0000000000000..b6780b1578d1e --- /dev/null +++ b/tests/cases/fourslash/completionsImportDefaultExportCrash.ts @@ -0,0 +1,42 @@ +/// + +// @module: nodenext +// @allowJs: true + +// @Filename: /node_modules/dom7/index.d.ts +//// export interface Dom7Array { +//// length: number; +//// prop(propName: string): any; +//// } +//// +//// export interface Dom7 { +//// (): Dom7Array; +//// fn: any; +//// } +//// +//// declare const Dom7: Dom7; +//// +//// export { +//// Dom7 as $, +//// }; + +// @Filename: /dom7.js +//// import * as methods from 'dom7'; +//// Object.keys(methods).forEach((methodName) => { +//// if (methodName === '$') return; +//// methods.$.fn[methodName] = methods[methodName]; +//// }); +//// +//// export default methods.$; + +// @Filename: /swipe-back.js +//// import $ from './dom7.js'; +//// /*1*/ + +verify.completions({ + marker: "1", + // some kind of a check should be added here + preferences: { + includeCompletionsForModuleExports: true, + } +}); diff --git a/tests/cases/fourslash/completionsImport_default_reExport.ts b/tests/cases/fourslash/completionsImport_default_reExport.ts new file mode 100644 index 0000000000000..cdb06c70461f9 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_default_reExport.ts @@ -0,0 +1,41 @@ +/// +// @module: commonjs +// @allowJs: true + +// @Filename: /file1.js +//// const a = 1; +//// export { +//// a as b +//// }; +//// export default a; + +// @Filename: /file2.js +//// import * as foo from './file1'; +//// /**/ +//// export default foo.b; + +goTo.marker(""); +verify.completions({ + marker: "", + exact: completion.globalsInJsPlus([ + "foo", + { + name: "a", + source: "./file1", + sourceDisplay: "./file1", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "b", + source: "./file1", + sourceDisplay: "./file1", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + } + ]), + preferences: { + includeCompletionsForModuleExports: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFix_reExportDefault.ts b/tests/cases/fourslash/importNameCodeFix_reExportDefault.ts index 29e587845a29f..4225c3164e7fb 100644 --- a/tests/cases/fourslash/importNameCodeFix_reExportDefault.ts +++ b/tests/cases/fourslash/importNameCodeFix_reExportDefault.ts @@ -6,6 +6,9 @@ // @Filename: /user2.ts ////unnamed; +// @Filename: /user3.ts +////reExportUnnamed; + // @Filename: /reExportNamed.ts ////export { default } from "./named"; @@ -38,3 +41,9 @@ unnamed;`, unnamed;`, ]); + +goTo.file("/user3.ts"); +verify.importFixAtPosition([ +`import reExportUnnamed from "./reExportUnnamed"; + +reExportUnnamed;`]);