diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 6034c9dfc3de1..4404665c94d6e 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1414,6 +1414,18 @@ function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: } /** @returns `Comparison.LessThan` if `a` is better than `b`. */ +/** Heuristic approach: Prioritize local/relative imports over node_modules imports. */ +function compareLocalVsExternal( + a: ImportFixWithModuleSpecifier, + b: ImportFixWithModuleSpecifier +): Comparison { + const aIsExternal = a.moduleSpecifierKind === "node_modules"; + const bIsExternal = b.moduleSpecifierKind === "node_modules"; + if (!aIsExternal && bIsExternal) return Comparison.LessThan; + if (aIsExternal && !bIsExternal) return Comparison.GreaterThan; + return Comparison.EqualTo; +} + function compareModuleSpecifiers( a: ImportFixWithModuleSpecifier, b: ImportFixWithModuleSpecifier, @@ -1424,6 +1436,11 @@ function compareModuleSpecifiers( toPath: (fileName: string) => Path, ): Comparison { if (a.kind !== ImportFixKind.UseNamespace && b.kind !== ImportFixKind.UseNamespace) { + + const localVsExternalComparison = compareLocalVsExternal(a, b); + if (localVsExternalComparison !== Comparison.EqualTo) { + return localVsExternalComparison; + } return compareBooleans( b.moduleSpecifierKind !== "node_modules" || allowsImportingSpecifier(b.moduleSpecifier), a.moduleSpecifierKind !== "node_modules" || allowsImportingSpecifier(a.moduleSpecifier), diff --git a/tests/cases/fourslash/importFixesPrioritizeLocal.ts b/tests/cases/fourslash/importFixesPrioritizeLocal.ts new file mode 100644 index 0000000000000..9f88eb53c99e6 --- /dev/null +++ b/tests/cases/fourslash/importFixesPrioritizeLocal.ts @@ -0,0 +1,54 @@ +/// + +// Test that local imports are prioritized over external node_modules imports +// when the same symbol is exported from both + +// @Filename: /node_modules/@mui/material/index.d.ts +//// export function useTheme(): { palette: string }; + +// @Filename: /node_modules/@mui/material/package.json +//// { "name": "@mui/material", "version": "5.0.0", "types": "index.d.ts" } + +// @Filename: /node_modules/zustand/index.d.ts +//// export function useStore(): any; + +// @Filename: /node_modules/zustand/package.json +//// { "name": "zustand", "version": "4.0.0", "types": "index.d.ts" } + +// @Filename: /utils/store.ts +//// export function useTheme() { +//// return { palette: 'light' }; +//// } +//// export function useStore() { +//// return {}; +//// } + +// @Filename: /components/Button.tsx +//// // Local useTheme should be suggested first +//// const theme = useTheme/*1*/(); + +// @Filename: /components/Header.tsx +//// // Local useStore should be suggested first +//// const store = useStore/*2*/(); + +// Test 1: useTheme - should prioritize local import over @mui/material +goTo.marker("1"); +verify.importFixAtPosition([ +`import { useTheme } from "../utils/store"; +// Local useTheme should be suggested first +const theme = useTheme();`, +`import { useTheme } from "@mui/material"; +// Local useTheme should be suggested first +const theme = useTheme();` +]); + +// Test 2: useStore - should prioritize local import over zustand +goTo.marker("2"); +verify.importFixAtPosition([ +`import { useStore } from "../utils/store"; +// Local useStore should be suggested first +const store = useStore();`, +`import { useStore } from "zustand"; +// Local useStore should be suggested first +const store = useStore();` +]); \ No newline at end of file