Allow moduleSymbolToValidIdentifier to be uppercase for JSX tags (#47625)

* Allow moduleSymbolToValidIdentifier to be uppercase for JSX tags

* Cleaner way of getting the uppercase name when needed

* Fix build errors, get rid of basically unnecessary ScriptTarget

* More accurate name for parameter

* Rename other parameter too

* Fix failing test
This commit is contained in:
Andrew Branch 2022-01-26 16:12:40 -08:00 committed by GitHub
parent 0d3ff0cce8
commit 5813a3541c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 105 additions and 26 deletions

View File

@ -74,7 +74,7 @@ namespace ts.codefix {
const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions));
const checker = program.getTypeChecker();
const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker));
const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, host, program, preferences, useAutoImportProvider);
const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, /*isJsxTagName*/ false, host, program, preferences, useAutoImportProvider);
const useRequire = shouldUseRequire(sourceFile, program);
const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
if (fix) {
@ -287,6 +287,7 @@ namespace ts.codefix {
moduleSymbol: Symbol,
sourceFile: SourceFile,
symbolName: string,
isJsxTagName: boolean,
host: LanguageServiceHost,
program: Program,
formatContext: formatting.FormatContext,
@ -296,7 +297,7 @@ namespace ts.codefix {
const compilerOptions = program.getCompilerOptions();
const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name))
? [getSymbolExportInfoForSymbol(targetSymbol, moduleSymbol, program, host)]
: getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true);
: getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, isJsxTagName, 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));
@ -349,7 +350,7 @@ namespace ts.codefix {
}
}
function getAllReExportingModules(importingFile: SourceFile, targetSymbol: 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, isJsxTagName: boolean, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
const result: SymbolExportInfo[] = [];
const compilerOptions = program.getCompilerOptions();
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
@ -364,7 +365,7 @@ namespace ts.codefix {
}
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && skipAlias(defaultInfo.symbol, checker) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === 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 });
}
@ -804,7 +805,7 @@ namespace ts.codefix {
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(symbolToken);
const useRequire = shouldUseRequire(sourceFile, program);
const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host, preferences);
const exportInfos = getExportInfos(symbolName, isJSXTagName(symbolToken), getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host, preferences);
const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) =>
getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences)));
return { fixes, symbolName };
@ -845,6 +846,7 @@ namespace ts.codefix {
// Returns a map from an exported symbol's ID to a list of every way it's (re-)exported.
function getExportInfos(
symbolName: string,
isJsxTagName: boolean,
currentTokenMeaning: SemanticMeaning,
cancellationToken: CancellationToken,
fromFile: SourceFile,
@ -876,7 +878,7 @@ namespace ts.codefix {
const compilerOptions = program.getCompilerOptions();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) {
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) {
addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson);
}
@ -1237,17 +1239,20 @@ namespace ts.codefix {
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
}
export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined): string {
return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target);
export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined, forceCapitalize: boolean): string {
return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target, forceCapitalize);
}
export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget | undefined): string {
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;

View File

@ -1520,7 +1520,6 @@ namespace ts.Completions {
source: string | undefined,
): CodeActionsAndSourceDisplay {
if (data?.moduleSpecifier) {
const { contextToken, previousToken } = getRelevantTokens(position, sourceFile);
if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementNode) {
// Import statement completion: 'import c|'
return { codeActions: undefined, sourceDisplay: [textPart(data.moduleSpecifier)] };
@ -1572,11 +1571,13 @@ namespace ts.Completions {
const checker = origin.isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker();
const { moduleSymbol } = origin;
const targetSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker));
const isJsxOpeningTagName = contextToken?.kind === SyntaxKind.LessThanToken && isJsxOpeningLikeElement(contextToken.parent);
const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction(
targetSymbol,
moduleSymbol,
sourceFile,
getNameForExportedSymbol(symbol, getEmitScriptTarget(compilerOptions)),
getNameForExportedSymbol(symbol, getEmitScriptTarget(compilerOptions), isJsxOpeningTagName),
isJsxOpeningTagName,
host,
program,
formatContext,
@ -2486,12 +2487,17 @@ namespace ts.Completions {
preferences,
!!importCompletionNode,
context => {
exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule, exportMapKey) => {
exportInfo.forEach(sourceFile.path, (info, getSymbolName, isFromAmbientModule, exportMapKey) => {
const symbolName = getSymbolName(/*preferCapitalized*/ isRightOfOpenTag);
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return;
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
// `targetFlags` should be the same for each `info`
if (!isTypeOnlyLocation && !importCompletionNode && !(info[0].targetFlags & SymbolFlags.Value)) return;
if (isTypeOnlyLocation && !(info[0].targetFlags & (SymbolFlags.Module | SymbolFlags.Type))) return;
// Do not try to auto-import something with a lowercase first letter for a JSX tag
const firstChar = symbolName.charCodeAt(0);
if (isRightOfOpenTag && (firstChar < CharacterCodes.A || firstChar > CharacterCodes.Z)) return;
const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name));
if (isCompletionDetailsMatch || !detailsEntryId && charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) {
const defaultExportInfo = find(info, isImportableExportInfo);
@ -2504,6 +2510,7 @@ namespace ts.Completions {
const { exportInfo = defaultExportInfo, moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {};
const isDefaultExport = exportInfo.exportKind === ExportKind.Default;
const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol;
pushAutoImportSymbol(symbol, {
kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export,
moduleSpecifier,

View File

@ -45,9 +45,9 @@ namespace ts {
export interface ExportInfoMap {
isUsableByFile(importingFile: Path): boolean;
clear(): void;
add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void;
add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, checker: TypeChecker): void;
get(importingFile: Path, key: string): readonly SymbolExportInfo[] | undefined;
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean, key: string) => void): void;
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], getSymbolName: (preferCapitalized?: boolean) => string, isFromAmbientModule: boolean, key: string) => void): void;
releaseSymbols(): void;
isEmpty(): boolean;
/** @returns Whether the change resulted in the cache being cleared */
@ -72,7 +72,7 @@ namespace ts {
symbols.clear();
usableByFileName = undefined;
},
add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => {
add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, checker) => {
if (importingFile !== usableByFileName) {
cache.clear();
usableByFileName = importingFile;
@ -88,7 +88,8 @@ namespace ts {
// get a better name.
const importedName = exportKind === ExportKind.Named || isExternalModuleSymbol(namedSymbol)
? unescapeLeadingUnderscores(symbolTableKey)
: getNameForExportedSymbol(namedSymbol, scriptTarget);
: getNameForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined);
const moduleName = stripQuotes(moduleSymbol.name);
const id = exportInfoId++;
const target = skipAlias(symbol, checker);
@ -119,7 +120,18 @@ namespace ts {
if (importingFile !== usableByFileName) return;
exportInfo.forEach((info, key) => {
const { symbolName, ambientModuleName } = parseKey(key);
action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName, key);
const rehydrated = info.map(rehydrateCachedInfo);
action(
rehydrated,
preferCapitalized => {
const { symbol, exportKind } = rehydrated[0];
const namedSymbol = exportKind === ExportKind.Default && getLocalSymbolForExportDefault(symbol) || symbol;
return preferCapitalized
? getNameForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined, /*preferCapitalized*/ true)
: symbolName;
},
!!ambientModuleName,
key);
});
},
releaseSymbols: () => {
@ -321,7 +333,6 @@ namespace ts {
host.log?.("getExportInfoMap: cache miss or empty; calculating new results");
const compilerOptions = program.getCompilerOptions();
const scriptTarget = getEmitScriptTarget(compilerOptions);
let moduleCount = 0;
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested();
@ -339,7 +350,6 @@ namespace ts {
moduleFile,
defaultInfo.exportKind,
isFromPackageJson,
scriptTarget,
checker);
}
checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => {
@ -352,7 +362,6 @@ namespace ts {
moduleFile,
ExportKind.Named,
isFromPackageJson,
scriptTarget,
checker);
}
});

View File

@ -3210,11 +3210,11 @@ namespace ts {
return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray;
}
export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined) {
export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined, preferCapitalized?: boolean) {
if (!(symbol.flags & SymbolFlags.Transient) && (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default)) {
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
return firstDefined(symbol.declarations, d => isExportAssignment(d) ? tryCast(skipOuterExpressions(d.expression), isIdentifier)?.text : undefined)
|| codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget);
|| codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, !!preferCapitalized);
}
return symbol.name;
}

View File

@ -87,8 +87,8 @@ namespace ts.projectSystem {
// transient symbols are recreated with every new checker.
const programBefore = project.getCurrentProgram()!;
let sigintPropBefore: readonly SymbolExportInfo[] | undefined;
exportMapCache.forEach(bTs.path as Path, (info, name) => {
if (name === "SIGINT") sigintPropBefore = info;
exportMapCache.forEach(bTs.path as Path, (info, getSymbolName) => {
if (getSymbolName() === "SIGINT") sigintPropBefore = info;
});
assert.ok(sigintPropBefore);
assert.ok(sigintPropBefore![0].symbol.flags & SymbolFlags.Transient);
@ -113,8 +113,8 @@ namespace ts.projectSystem {
// Get same info from cache again
let sigintPropAfter: readonly SymbolExportInfo[] | undefined;
exportMapCache.forEach(bTs.path as Path, (info, name) => {
if (name === "SIGINT") sigintPropAfter = info;
exportMapCache.forEach(bTs.path as Path, (info, getSymbolName) => {
if (getSymbolName() === "SIGINT") sigintPropAfter = info;
});
assert.ok(sigintPropAfter);
assert.notEqual(symbolIdBefore, getSymbolId(sigintPropAfter![0].symbol));

View File

@ -0,0 +1,39 @@
/// <reference path="fourslash.ts" />
// @module: commonjs
// @jsx: react
// @Filename: /component.tsx
//// export default function (props: any) {}
// @Filename: /index.tsx
//// export function Index() {
//// return <Component/**/
//// }
goTo.marker("");
verify.completions({
marker: "",
includes: {
name: "Component",
source: "/component",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions,
},
excludes: "component",
preferences: {
includeCompletionsForModuleExports: true,
},
});
verify.applyCodeActionFromCompletion("", {
name: "Component",
source: "/component",
description: `Import default 'Component' from module "./component"`,
newFileContent:
`import Component from "./component";
export function Index() {
return <Component
}`,
});

View File

@ -0,0 +1,19 @@
/// <reference path="fourslash.ts" />
// @module: commonjs
// @jsx: react-jsx
// @Filename: /component.tsx
//// export default function (props: any) {}
// @Filename: /index.tsx
//// export function Index() {
//// return <Component/**/ />;
//// }
goTo.marker("");
verify.importFixAtPosition([`import Component from "./component";
export function Index() {
return <Component />;
}`]);