Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 25, 2026

Auto-import was suggesting internal files from symlinked monorepo packages when the package has an exports field that doesn't expose them. Accepting such suggestions produces unresolvable imports like import { fooInternal } from "@packages/foo/src/internal/index.js".

Changes

  • ResolvedEntrypoints.PackageHasExports - New field to track whether a package has an exports field during entrypoint generation
  • RegistryBucket.PackagesWithExports - Set for O(1) lookup of packages with exports during specifier generation
  • GetModuleSpecifier - Skip EndingChangeable entrypoints (directory-scanned files not in exports) when package has exports
  • isInvalidPackageSpecifier - Filter bare specifiers with subpaths that aren't valid entrypoints in project bucket path

Test case

Added TestAutoImportTransitiveLeak reproducing the scenario:

packages/foo/package.json:  exports: { ".": "./src/index.ts" }
packages/foo/src/index.ts:  import { fooInternal } from "#internal/index"; export function foo() { ... }
packages/foo/src/internal/index.ts:  export function fooInternal() { ... }

// In packages/bar, typing "fooInt" should NOT suggest fooInternal
Original prompt

This section details on the original issue you should resolve

<issue_title>Autocomplete incorrectly suggests transitive files from monorepo package</issue_title>
<issue_description>### Extension Version

0.20260124.1

VS Code Version

1.108.2

Operating system Version

macOS 15.7

Steps to reproduce

I've written a failing fourslash test to demonstrate the issue:

package fourslash_test

import (
	"testing"

	"github.com/microsoft/typescript-go/internal/fourslash"
	. "github.com/microsoft/typescript-go/internal/fourslash/tests/util"
	"github.com/microsoft/typescript-go/internal/ls"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/testutil"
)

const TestAutoImportTransitiveLeakScenario = `
// @Filename: /home/src/workspaces/project/tsconfig.base.json
{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "composite": true
  }
}

// @Filename: /home/src/workspaces/project/packages/foo/package.json
{
  "name": "@packages/foo",
  "type": "module",
  "exports": {
    ".": {
	  "types": "./src/index.ts",
	  "default": "./dist/index.js"
	}
  },
  "imports": {
    "#*": {
	  "types": "./src/*.ts",
      "default": "./dist/*.js"
    }
  }
}

// @Filename: /home/src/workspaces/project/packages/foo/tsconfig.json
{ "extends": "../../tsconfig.base.json" }

// @Filename: /home/src/workspaces/project/packages/foo/src/internal/index.ts
export function fooInternal() {
  console.log("foo");
}

// @Filename: /home/src/workspaces/project/packages/foo/src/index.ts
import { fooInternal } from "#internal/index"
export function foo() {
  fooInternal();
}

// @Filename: /home/src/workspaces/project/packages/bar/package.json
{
  "name": "@packages/bar",
  "type": "module",
  "exports": {
    ".": {
	  "types": "./src/index.ts",
	  "default": "./dist/index.js"
	}
  },
  "imports": {
    "#*": {
	  "types": "./src/*.ts",
      "default": "./dist/*.js"
    }
  },
  "dependencies": {
    "@packages/foo": "*"
  }
}

// @Filename: /home/src/workspaces/project/packages/bar/tsconfig.json
{ "extends": "../../tsconfig.base.json" }

// @Filename: /home/src/workspaces/project/packages/bar/src/index.ts
import { foo } from "@packages/foo"

fo/*fooCompletion*/

// @Filename: /home/src/workspaces/project/package.json
{ "workspaces": ["packages/*"], "type": "module" }

// @link: /home/src/workspaces/project/packages/bar -> /home/src/workspaces/project/node_modules/@packages/bar
// @link: /home/src/workspaces/project/packages/foo -> /home/src/workspaces/project/node_modules/@packages/foo
`

func TestAutoImportTransitiveLeak(t *testing.T) {
	t.Parallel()

	defer testutil.RecoverAndFail(t, "Panic on fourslash test")

	f, done := fourslash.NewFourslash(t, nil /*capabilities*/, TestAutoImportTransitiveLeakScenario)

	defer done()

	f.VerifyCompletions(t, "fooCompletion", &fourslash.CompletionsExpectedList{
		IsIncomplete: false,
		ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
			CommitCharacters: &DefaultCommitCharacters,
			EditRange:        Ignored,
		},
		Items: &fourslash.CompletionsExpectedItems{
			Includes: []fourslash.CompletionsExpectedItem{
				&lsproto.CompletionItem{
					Label:               "foo",
					AdditionalTextEdits: fourslash.AnyTextEdits,
					SortText: PtrTo(string(ls.SortTextLocationPriority)),
				},
			},
			Excludes: []string{"fooInternal"},
		},
	})
}

I placed it at internal/fourslash/tests/autoImportTransitiveLeak_test.go.

Issue

Similar to microsoft/TypeScript#63033, but I've reproduced it here. I don't believe that it should suggest fooInternal as an autocomplete, as it's not actually exported. What does happen, however, is that it suggests fooInternal, and accepting it produces the following code with a red squiggly line on the second import:

import { foo } from "@packages/foo";
import { fooInternal } from "@packages/foo/src/internal/index.js";

fooInternal

The diagnostic then says Cannot find module '@packages/foo/src/internal/index.js' or its corresponding type declarations.

Additional information about the issue

This was very difficult for me to reproduce, but I believe I've identified two variables that prevent the bug from surfacing:

  • Add a version field to the packages/foo/package.json, or
  • Add "preserveSymlinks": true to tsconfig.base.json</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

…o packages

When a package has an "exports" field in its package.json, only files
explicitly exposed via exports should be importable. Previously,
internal files that were transitively imported within a package were
incorrectly suggested for auto-import, resulting in import statements
that wouldn't resolve.

The fix:
1. Added PackageHasExports field to ResolvedEntrypoints to track whether
   a package has an exports field
2. Added PackagesWithExports set to RegistryBucket to efficiently lookup
   which packages have exports
3. Modified GetModuleSpecifier to skip EndingChangeable entrypoints
   (files discovered by directory scanning rather than exports) when
   the package has an exports field
4. Added isInvalidPackageSpecifier to filter out bare package specifiers
   with subpaths that don't resolve to valid entrypoints

Added test case TestAutoImportTransitiveLeak that reproduces the issue.

Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix autocomplete suggestions for transitive files Fix autocomplete suggesting unexported internal files from monorepo packages Jan 25, 2026
@ericnorris
Copy link

Thanks @DanielRosenwasser! It might be worth noting that I believe having a version field prevents the auto-import from leaking the transitive files. I am not very familiar with this codebase, but from what I can see this may be because the getPackageId function returns an empty PackageId{} if the version field is missing, which then causes other code to look at the real path, which ignores exports. I wonder if one approach would be to allow an empty version field, but that may be too simple.

@andrewbranch
Copy link
Member

@copilot this doesn’t look right. Let me tell you how I think it should work and then you investigate and tell me which assumption is breaking down:

  • module.GetResolvedEntrypoints should already not do any directory searching for packages that have "exports" (unless the directory search is directed by an export with a * in the key and path). There’s no need for a HasExports bool because the return value should only ever contain reachable entrypoints.
  • In (*autoimport.View).GetModuleSpecifier, if we are processing an Export without a ResolvedEntrypoint with a pre-computed package specifier, we should fall back to the internal/modulespecifiers generation logic, which should also respect "exports" and never produce an invalid bare specifier.
  • I have no idea why package.json version is relevant as @ericnorris reported, so it would be good to investigate that.

Please do a thorough investigation of the root cause and report back your findings.

@andrewbranch
Copy link
Member

@copilot try again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Autocomplete incorrectly suggests transitive files from monorepo package

4 participants