Skip to content
Closed
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
25 changes: 14 additions & 11 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3692,9 +3692,12 @@ export function isInExpressionContext(node: Node): boolean {
case SyntaxKind.ShorthandPropertyAssignment:
// TODO(jakebailey): it's possible that node could be the name, too
return (parent as ShorthandPropertyAssignment).objectAssignmentInitializer === node;
case SyntaxKind.SatisfiesExpression:
return node === (parent as SatisfiesExpression).expression;
default:
case SyntaxKind.SatisfiesExpression:
return node === (parent as SatisfiesExpression).expression;
case SyntaxKind.ElementAccessExpression:
const elementAccess = parent as ElementAccessExpression;
return node === elementAccess.expression || node === elementAccess.argumentExpression;
default:
return isExpressionNode(parent);
}
}
Expand Down Expand Up @@ -10502,14 +10505,14 @@ export function isValidBigIntString(s: string, roundTripOnly: boolean): boolean
&& (!roundTripOnly || s === pseudoBigIntToString({ negative, base10Value: parsePseudoBigInt(scanner.getTokenValue()) }));
}

/** @internal */
export function isValidTypeOnlyAliasUseSite(useSite: Node): boolean {
return !!(useSite.flags & NodeFlags.Ambient)
|| isInJSDoc(useSite)
|| isPartOfTypeQuery(useSite)
|| isIdentifierInNonEmittingHeritageClause(useSite)
|| isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(useSite)
|| !(isExpressionNode(useSite) || isShorthandPropertyNameUseSite(useSite));
/** @internal */
export function isValidTypeOnlyAliasUseSite(useSite: Node): boolean {
return !!(useSite.flags & NodeFlags.Ambient)
|| isInJSDoc(useSite)
|| isPartOfTypeQuery(useSite)
|| isIdentifierInNonEmittingHeritageClause(useSite)
|| isPartOfPossiblyValidTypeOrAbstractComputedPropertyName(useSite)
|| !(isExpressionNode(useSite) || isShorthandPropertyNameUseSite(useSite) || isInExpressionContext(useSite));
}

function isShorthandPropertyNameUseSite(useSite: Node) {
Expand Down
66 changes: 35 additions & 31 deletions src/services/codefixes/importFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,29 +1073,29 @@ function getNamespaceLikeImportText(declaration: AnyImportOrRequire) {
}
}

function getAddAsTypeOnly(
isValidTypeOnlyUseSite: boolean,
isForNewImportDeclaration: boolean,
symbol: Symbol | undefined,
targetFlags: SymbolFlags,
checker: TypeChecker,
compilerOptions: CompilerOptions,
) {
if (!isValidTypeOnlyUseSite) {
// Can't use a type-only import if the usage is an emitting position
return AddAsTypeOnly.NotAllowed;
}
if (
symbol &&
compilerOptions.verbatimModuleSyntax &&
(!(targetFlags & SymbolFlags.Value) || !!checker.getTypeOnlyAliasDeclaration(symbol))
) {
// A type-only import is required for this symbol if under these settings if the symbol will
// be erased, which will happen if the target symbol is purely a type or if it was exported/imported
// as type-only already somewhere between this import and the target.
return AddAsTypeOnly.Required;
}
return AddAsTypeOnly.Allowed;
function getAddAsTypeOnly(
isValidTypeOnlyUseSite: boolean,
isForNewImportDeclaration: boolean,
symbol: Symbol | undefined,
targetFlags: SymbolFlags,
checker: TypeChecker,
compilerOptions: CompilerOptions,
) {
if (!isValidTypeOnlyUseSite) {
// Can't use a type-only import if the usage is an emitting position
return AddAsTypeOnly.NotAllowed;
}
if (
symbol &&
compilerOptions.verbatimModuleSyntax &&
(!(targetFlags & SymbolFlags.Value) || !!checker.getTypeOnlyAliasDeclaration(symbol))
) {
// A type-only import is required for this symbol if under these settings if the symbol will
// be erased, which will happen if the target symbol is purely a type or if it was exported/imported
// as type-only already somewhere between this import and the target.
return AddAsTypeOnly.Required;
}
return AddAsTypeOnly.Allowed;
}

function tryAddToExistingImport(existingImports: readonly FixAddToExistingImportInfo[], isValidTypeOnlyUseSite: boolean, checker: TypeChecker, compilerOptions: CompilerOptions): FixAddToExistingImport | undefined {
Expand All @@ -1104,14 +1104,18 @@ function tryAddToExistingImport(existingImports: readonly FixAddToExistingImport
const fix = getAddToExistingImportFix(existingImport);
if (!fix) continue;
const isTypeOnly = isTypeOnlyImportDeclaration(fix.importClauseOrBindingPattern);
if (
fix.addAsTypeOnly !== AddAsTypeOnly.NotAllowed && isTypeOnly ||
fix.addAsTypeOnly === AddAsTypeOnly.NotAllowed && !isTypeOnly
) {
// Give preference to putting types in existing type-only imports and avoiding conversions
// of import statements to/from type-only.
return fix;
}
if (
fix.addAsTypeOnly !== AddAsTypeOnly.NotAllowed && isTypeOnly ||
fix.addAsTypeOnly === AddAsTypeOnly.NotAllowed && !isTypeOnly
) {
// Give preference to putting types in existing type-only imports and avoiding conversions
// of import statements to/from type-only.
return fix;
}
// Don't add to existing type-only imports if the symbol should be imported as a value
if (fix.addAsTypeOnly === AddAsTypeOnly.NotAllowed && isTypeOnly) {
continue; // Skip this import, don't even consider it as a fallback
}
best ??= fix;
}
return best;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// <reference path="fourslash.ts" />

// Test for issue #61894: Symbol auto-import bug with existing type-only imports
// Verifies that symbols used in element access expressions are imported as values,
// not added to existing type-only imports, when completing inside thing[...]

// @noLib: true

// @Filename: /globals.d.ts
////declare const Symbol: { for(key: string): symbol };

// @Filename: /exportsSymbol.ts
////export const SYM_FOO_BAR = Symbol.for("foo.bar");
////
////export interface ObjWithSym {
//// [SYM_FOO_BAR]: any;
////}

// @Filename: /usesSymbol.ts
////import type { ObjWithSym } from "./exportsSymbol";
////
////export declare const thing: ObjWithSym;
////
////function main() {
//// thing[|./**/|]
////}

verify.completions({
marker: "",
includes: [
{ name: "SYM_FOO_BAR", source: "/exportsSymbol", insertText: "[SYM_FOO_BAR]", replacementSpan: test.ranges()[0], hasAction: true },
],
preferences: {
includeInsertTextCompletions: true,
includeCompletionsForModuleExports: true,
},
});

// Verify the fix - the symbol should be imported as a value, not as a type
verify.applyCodeActionFromCompletion("", {
name: "SYM_FOO_BAR",
source: "/exportsSymbol",
description: `Update import from "./exportsSymbol"`,
newFileContent:
`import { SYM_FOO_BAR, type ObjWithSym } from "./exportsSymbol";

export declare const thing: ObjWithSym;

function main() {
thing.
}`
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/// <reference path="fourslash.ts" />

// Control test for issue #61894: Symbol auto-import without existing type-only imports
// Verifies that symbols are correctly imported as values when there are no existing imports

// @noLib: true

// @Filename: /globals.d.ts
////declare const Symbol: { for(key: string): symbol };

// @Filename: /exportsSymbol.ts
////export const SYM_FOO_BAR = Symbol.for("foo.bar");
////
////export interface ObjWithSym {
//// [SYM_FOO_BAR]: any;
////}

// @Filename: /usesSymbol.ts
////export declare const thing: import("./exportsSymbol").ObjWithSym;
////
////function main() {
//// thing[|./**/|]
////}

verify.completions({
marker: "",
includes: [
{ name: "SYM_FOO_BAR", source: "/exportsSymbol", insertText: "[SYM_FOO_BAR]", replacementSpan: test.ranges()[0], hasAction: true },
],
preferences: {
includeInsertTextCompletions: true,
includeCompletionsForModuleExports: true,
},
});

verify.applyCodeActionFromCompletion("", {
name: "SYM_FOO_BAR",
source: "/exportsSymbol",
description: `Add import from "./exportsSymbol"`,
newFileContent:
`import { SYM_FOO_BAR } from "./exportsSymbol";

export declare const thing: import("./exportsSymbol").ObjWithSym;

function main() {
thing.
}`
});
Loading