Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3656,8 +3656,8 @@ namespace ts {
if (exportEquals !== moduleSymbol) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is forEachExportAndPropertyOfModule only used in the services layer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

const type = getTypeOfSymbol(exportEquals);
if (shouldTreatPropertiesOfExternalModuleAsExports(type)) {
getPropertiesOfType(type).forEach(symbol => {
cb(symbol, symbol.escapedName);
forEachPropertyOfType(type, (symbol, escapedName) => {
cb(symbol, escapedName);
});
}
}
Expand Down Expand Up @@ -4033,13 +4033,17 @@ namespace ts {
function getNamedMembers(members: SymbolTable): Symbol[] {
let result: Symbol[] | undefined;
members.forEach((symbol, id) => {
if (!isReservedMemberName(id) && symbolIsValue(symbol)) {
if (isNamedMember(symbol, id)) {
(result || (result = [])).push(symbol);
}
});
return result || emptyArray;
}

function isNamedMember(member: Symbol, escapedName: __String) {
return !isReservedMemberName(escapedName) && symbolIsValue(member);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does symbolIsValue do, and how is it useful here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just checks symbol.flags & SymbolFlags.Value, and it’s used to filter out type and uninstantiated namespace members out of instantiated namespaces. (I just moved this existing logic into a function so I could reuse it.)

}

function getNamedOrIndexSignatureMembers(members: SymbolTable): Symbol[] {
const result = getNamedMembers(members);
const index = getIndexSymbolFromSymbolTable(members);
Expand Down Expand Up @@ -11571,6 +11575,17 @@ namespace ts {
getPropertiesOfObjectType(type);
}

function forEachPropertyOfType(type: Type, action: (symbol: Symbol, escapedName: __String) => void): void {
type = getReducedApparentType(type);
if (type.flags & TypeFlags.StructuredType) {
resolveStructuredTypeMembers(type as StructuredType).members.forEach((symbol, escapedName) => {
if (isNamedMember(symbol, escapedName)) {
action(symbol, escapedName);
}
});
}
}

function isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes): boolean {
const list = obj.properties as NodeArray<ObjectLiteralElementLike | JsxAttributeLike>;
return list.some(property => {
Expand Down
12 changes: 6 additions & 6 deletions src/services/codefixes/importFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ namespace ts.codefix {
}

export function getImportCompletionAction(
exportedSymbol: Symbol,
targetSymbol: Symbol,
moduleSymbol: Symbol,
sourceFile: SourceFile,
symbolName: string,
Expand All @@ -280,8 +280,8 @@ namespace ts.codefix {
): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } {
const compilerOptions = program.getCompilerOptions();
const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name))
? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, program, host)]
: getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true);
? [getSymbolExportInfoForSymbol(targetSymbol, moduleSymbol, program, host)]
: getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true);
const useRequire = shouldUseRequire(sourceFile, program);
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, isValidTypeOnlyUseSite, useRequire, host, preferences));
Expand Down Expand Up @@ -326,7 +326,7 @@ namespace ts.codefix {
}
}

function getAllReExportingModules(importingFile: SourceFile, exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
function getAllReExportingModules(importingFile: SourceFile, targetSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
const result: SymbolExportInfo[] = [];
const compilerOptions = program.getCompilerOptions();
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
Expand All @@ -341,12 +341,12 @@ namespace ts.codefix {
}

const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && skipAlias(defaultInfo.symbol, checker) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
result.push({ symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(defaultInfo.symbol, checker).flags, isFromPackageJson });
}

for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
if (exported.name === symbolName && checker.getMergedSymbol(skipAlias(exported, checker)) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
result.push({ symbol: exported, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: ExportKind.Named, targetFlags: skipAlias(exported, checker).flags, isFromPackageJson });
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1183,9 +1183,9 @@ namespace ts.Completions {

const checker = origin.isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker();
const { moduleSymbol } = origin;
const exportedSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker));
const targetSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker));
const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction(
exportedSymbol,
targetSymbol,
moduleSymbol,
sourceFile,
getNameForExportedSymbol(symbol, getEmitScriptTarget(compilerOptions)),
Expand Down
16 changes: 10 additions & 6 deletions src/services/exportInfoMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ namespace ts {
}
const isDefault = exportKind === ExportKind.Default;
const namedSymbol = isDefault && getLocalSymbolForExportDefault(symbol) || symbol;
// A re-export merged with an export from a module augmentation can result in `symbol`
// being an external module symbol; the name it is re-exported by will be `symbolTableKey`
// (which comes from the keys of `moduleSymbol.exports`.)
const importedName = isExternalModuleSymbol(namedSymbol)
// 1. A named export must be imported by its key in `moduleSymbol.exports` or `moduleSymbol.members`.
// 2. A re-export merged with an export from a module augmentation can result in `symbol`
// being an external module symbol; the name it is re-exported by will be `symbolTableKey`
// (which comes from the keys of `moduleSymbol.exports`.)
// 3. Otherwise, we have a default/namespace import that can be imported by any name, and
// `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to
// get a better name.
const importedName = exportKind === ExportKind.Named || isExternalModuleSymbol(namedSymbol)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine, but it feels a little backward compared to the usual approach of checking for special names first

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably agree—this is a refactor of a refactor of a refactor of... it has probably accumulated some unnecessary roundabout logic that could be simplified if rewritten fresh.

? unescapeLeadingUnderscores(symbolTableKey)
: getNameForExportedSymbol(namedSymbol, scriptTarget);
const moduleName = stripQuotes(moduleSymbol.name);
Expand Down Expand Up @@ -321,7 +325,7 @@ namespace ts {
let moduleCount = 0;
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested();
const seenExports = new Map<Symbol, true>();
const seenExports = new Map<__String, true>();
const checker = program.getTypeChecker();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
// Note: I think we shouldn't actually see resolved module symbols here, but weird merges
Expand All @@ -339,7 +343,7 @@ namespace ts {
checker);
}
checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => {
if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, exported)) {
if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, key)) {
cache.add(
importingFile.path,
exported,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/// <reference path="../fourslash.ts" />

// @Filename: /tsconfig.json
//// { "compilerOptions": { "module": "commonjs", "allowJs": true } }

// @Filename: /third_party/marked/src/defaults.js
//// function getDefaults() {
//// return {
//// baseUrl: null,
//// };
//// }
////
//// function changeDefaults(newDefaults) {
//// module.exports.defaults = newDefaults;
//// }
////
//// module.exports = {
//// defaults: getDefaults(),
//// getDefaults,
//// changeDefaults
//// };

// @Filename: /index.ts
//// /**/

format.setOption("newLineCharacter", "\n")
goTo.marker("");

// Create the exportInfoMap
verify.completions({ marker: "", preferences: { includeCompletionsForModuleExports: true } });

// Create a new program and reuse the exportInfoMap from the last request
edit.insert("d");
verify.completions({
marker: "",
excludes: ["newDefaults"],
includes: [{
name: "defaults",
source: "/third_party/marked/src/defaults",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions,
}],
preferences: { includeCompletionsForModuleExports: true }
});

verify.applyCodeActionFromCompletion("", {
name: "defaults",
source: "/third_party/marked/src/defaults",
description: `Import 'defaults' from module "./third_party/marked/src/defaults"`,
data: {
exportName: "defaults",
fileName: "/third_party/marked/src/defaults.js",
},
newFileContent: `import { defaults } from "./third_party/marked/src/defaults";

d`
});