Skip to content

Commit af2bc58

Browse files
committed
fix #3271: bad import-is-undefined warning in ts
1 parent 2292036 commit af2bc58

File tree

7 files changed

+172
-19
lines changed

7 files changed

+172
-19
lines changed

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
* Adjust esbuild's warning about undefined imports for TypeScript `import` equals declarations ([#3271](https://github.com/evanw/esbuild/issues/3271))
6+
7+
In JavaScript, accessing a missing property on an import namespace object is supposed to result in a value of `undefined` at run-time instead of an error at compile-time. This is something that esbuild warns you about by default because doing this can indicate a bug with your code. For example:
8+
9+
```js
10+
// app.js
11+
import * as styles from './styles'
12+
console.log(styles.buton)
13+
```
14+
15+
```js
16+
// styles.js
17+
export let button = {}
18+
```
19+
20+
If you bundle `app.js` with esbuild you will get this:
21+
22+
```
23+
▲ [WARNING] Import "buton" will always be undefined because there is no matching export in "styles.js" [import-is-undefined]
24+
25+
app.js:2:19:
26+
2console.log(styles.buton)
27+
~~~~~
28+
╵ button
29+
30+
Did you mean to import "button" instead?
31+
32+
styles.js:1:11:
33+
1export let button = {}
34+
~~~~~~
35+
```
36+
37+
However, there is TypeScript-only syntax for `import` equals declarations that can represent either a type import (which esbuild should ignore) or a value import (which esbuild should respect). Since esbuild doesn't have a type system, it tries to only respect `import` equals declarations that are actually used as values. Previously esbuild always generated this warning for unused imports referenced within `import` equals declarations even when the reference could be a type instead of a value. Starting with this release, esbuild will now only warn in this case if the import is actually used. Here is an example of some code that no longer causes an incorrect warning:
38+
39+
```ts
40+
// app.ts
41+
import * as styles from './styles'
42+
import ButtonType = styles.Button
43+
```
44+
45+
```ts
46+
// styles.ts
47+
export interface Button {}
48+
```
49+
350
## 0.18.16
451
552
* Fix a regression with whitespace inside `:is()` ([#3265](https://github.com/evanw/esbuild/issues/3265))

internal/bundler_tests/bundler_importstar_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1668,21 +1668,29 @@ internal-ns-def.js: WARNING: Import "def" will always be undefined because there
16681668
}
16691669

16701670
func TestImportDefaultNamespaceComboNoDefault(t *testing.T) {
1671+
// Note: "entry-dead.js" checks that this warning doesn't happen for dead code
16711672
importstar_suite.expectBundled(t, bundled{
16721673
files: map[string]string{
16731674
"/entry-default-ns-prop.js": `import def, * as ns from './foo'; console.log(def, ns, ns.default)`,
16741675
"/entry-default-ns.js": `import def, * as ns from './foo'; console.log(def, ns)`,
16751676
"/entry-default-prop.js": `import def, * as ns from './foo'; console.log(def, ns.default)`,
16761677
"/entry-default.js": `import def from './foo'; console.log(def)`,
16771678
"/entry-prop.js": `import * as ns from './foo'; console.log(ns.default)`,
1678-
"/foo.js": `export let foo = 123`,
1679+
"/entry-dead.js": `import * as ns from './foo'; 0 && console.log(ns.default)`,
1680+
"/entry-typo.js": `import * as ns from './foo'; console.log(ns.buton)`,
1681+
"/entry-typo-indirect.js": `import * as ns from './indirect'; console.log(ns.buton)`,
1682+
"/foo.js": `export let button = {}`,
1683+
"/indirect.js": `export * from './foo'`,
16791684
},
16801685
entryPaths: []string{
16811686
"/entry-default-ns-prop.js",
16821687
"/entry-default-ns.js",
16831688
"/entry-default-prop.js",
16841689
"/entry-default.js",
16851690
"/entry-prop.js",
1691+
"/entry-dead.js",
1692+
"/entry-typo.js",
1693+
"/entry-typo-indirect.js",
16861694
},
16871695
options: config.Options{
16881696
Mode: config.ModeBundle,
@@ -1695,6 +1703,10 @@ entry-default-prop.js: ERROR: No matching export in "foo.js" for import "default
16951703
entry-default-prop.js: WARNING: Import "default" will always be undefined because there is no matching export in "foo.js"
16961704
entry-default.js: ERROR: No matching export in "foo.js" for import "default"
16971705
entry-prop.js: WARNING: Import "default" will always be undefined because there is no matching export in "foo.js"
1706+
entry-typo-indirect.js: WARNING: Import "buton" will always be undefined because there is no matching export in "indirect.js"
1707+
foo.js: NOTE: Did you mean to import "button" instead?
1708+
entry-typo.js: WARNING: Import "buton" will always be undefined because there is no matching export in "foo.js"
1709+
foo.js: NOTE: Did you mean to import "button" instead?
16981710
`,
16991711
})
17001712
}

internal/bundler_tests/bundler_ts_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,36 @@ func TestTSImportEqualsBundle(t *testing.T) {
867867
})
868868
}
869869

870+
func TestTSImportEqualsUndefinedImport(t *testing.T) {
871+
ts_suite.expectBundled(t, bundled{
872+
files: map[string]string{
873+
"/entry.ts": `
874+
import * as ns from './import.ts'
875+
import value_copy = ns.value
876+
import Type_copy = ns.Type
877+
let foo: Type_copy = value_copy
878+
console.log(foo)
879+
`,
880+
"/import.ts": `
881+
export let value = 123
882+
export type Type = number
883+
`,
884+
},
885+
entryPaths: []string{"/entry.ts"},
886+
options: config.Options{
887+
Mode: config.ModeBundle,
888+
AbsOutputFile: "/out.js",
889+
ExternalSettings: config.ExternalSettings{
890+
PreResolve: config.ExternalMatchers{
891+
Exact: map[string]bool{
892+
"pkg": true,
893+
},
894+
},
895+
},
896+
},
897+
})
898+
}
899+
870900
func TestTSMinifiedBundleES6(t *testing.T) {
871901
ts_suite.expectBundled(t, bundled{
872902
files: map[string]string{

internal/bundler_tests/snapshots/snapshots_ts.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1491,7 +1491,6 @@ TestTSImportEqualsTreeShakingFalse
14911491
---------- /out.js ----------
14921492
import { foo } from "pkg";
14931493
const used = foo.used;
1494-
const unused = foo.unused;
14951494
export { used };
14961495

14971496
================================================================================
@@ -1501,6 +1500,17 @@ import { foo } from "pkg";
15011500
const used = foo.used;
15021501
export { used };
15031502

1503+
================================================================================
1504+
TestTSImportEqualsUndefinedImport
1505+
---------- /out.js ----------
1506+
// import.ts
1507+
var value = 123;
1508+
1509+
// entry.ts
1510+
var value_copy = value;
1511+
var foo = value_copy;
1512+
console.log(foo);
1513+
15041514
================================================================================
15051515
TestTSImportMTS
15061516
---------- /out.js ----------

internal/graph/meta.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package graph
55

66
import (
77
"github.com/evanw/esbuild/internal/ast"
8+
"github.com/evanw/esbuild/internal/helpers"
89
"github.com/evanw/esbuild/internal/js_ast"
910
"github.com/evanw/esbuild/internal/logger"
1011
)
@@ -92,8 +93,9 @@ type JSReprMeta struct {
9293
//
9394
// Re-exports come from other files and are the result of resolving export
9495
// star statements (i.e. "export * from 'foo'").
95-
ResolvedExports map[string]ExportData
96-
ResolvedExportStar *ExportData
96+
ResolvedExports map[string]ExportData
97+
ResolvedExportStar *ExportData
98+
ResolvedExportTypos *helpers.TypoDetector
9799

98100
// Never iterate over "resolvedExports" directly. Instead, iterate over this
99101
// array. Some exports in that map aren't meant to end up in generated code.

internal/js_parser/js_parser.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15741,11 +15741,18 @@ func (p *parser) checkForUnusedTSImportEquals(s *js_ast.SLocal, result *importsE
1574115741
}
1574215742

1574315743
// Is this an identifier reference and not a require() call?
15744-
if id, ok := value.Data.(*js_ast.EIdentifier); ok {
15744+
valueRef := ast.InvalidRef
15745+
switch v := value.Data.(type) {
15746+
case *js_ast.EIdentifier:
15747+
valueRef = v.Ref
15748+
case *js_ast.EImportIdentifier:
15749+
valueRef = v.Ref
15750+
}
15751+
if valueRef != ast.InvalidRef {
1574515752
// Is this import statement unused?
1574615753
if ref := decl.Binding.Data.(*js_ast.BIdentifier).Ref; p.symbols[ref.InnerIndex].UseCountEstimate == 0 {
1574715754
// Also don't count the referenced identifier
15748-
p.ignoreUsage(id.Ref)
15755+
p.ignoreUsage(valueRef)
1574915756

1575015757
// Import-equals statements can come in any order. Removing one
1575115758
// could potentially cause another one to be removable too.

internal/linker/linker.go

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2506,21 +2506,66 @@ loop:
25062506

25072507
// Report mismatched imports and exports
25082508
if symbol.ImportItemStatus == ast.ImportItemGenerated {
2509-
// This is a debug message instead of an error because although it
2510-
// appears to be a named import, it's actually an automatically-
2511-
// generated named import that was originally a property access on an
2512-
// import star namespace object. Normally this property access would
2513-
// just resolve to undefined at run-time instead of failing at binding-
2514-
// time, so we emit a debug message and rewrite the value to the literal
2515-
// "undefined" instead of emitting an error.
2509+
// This is not an error because although it appears to be a named
2510+
// import, it's actually an automatically-generated named import
2511+
// that was originally a property access on an import star
2512+
// namespace object:
2513+
//
2514+
// import * as ns from 'foo'
2515+
// const undefinedValue = ns.notAnExport
2516+
//
2517+
// If this code wasn't bundled, this property access would just resolve
2518+
// to undefined at run-time instead of failing at binding-time, so we
2519+
// emit rewrite the value to the literal "undefined" instead of
2520+
// emitting an error.
25162521
symbol.ImportItemStatus = ast.ImportItemMissing
2517-
kind := logger.Warning
2518-
if helpers.IsInsideNodeModules(trackerFile.InputFile.Source.KeyPath.Text) {
2519-
kind = logger.Debug
2522+
2523+
// Don't emit a log message if this symbol isn't used, since then the
2524+
// log message isn't helpful. This can happen with "import" assignment
2525+
// statements in TypeScript code since they are ambiguously either a
2526+
// type or a value. We consider them to be a type if they aren't used.
2527+
//
2528+
// import * as ns from 'foo'
2529+
//
2530+
// // There's no warning here because this is dead code
2531+
// if (false) ns.notAnExport
2532+
//
2533+
// // There's no warning here because this is never used
2534+
// import unused = ns.notAnExport
2535+
//
2536+
if symbol.UseCountEstimate > 0 {
2537+
nextFile := &c.graph.Files[nextTracker.sourceIndex].InputFile
2538+
msg := logger.Msg{
2539+
Kind: logger.Warning,
2540+
Data: trackerFile.LineColumnTracker().MsgData(r, fmt.Sprintf(
2541+
"Import %q will always be undefined because there is no matching export in %q",
2542+
namedImport.Alias, nextFile.Source.PrettyPath)),
2543+
}
2544+
if helpers.IsInsideNodeModules(trackerFile.InputFile.Source.KeyPath.Text) {
2545+
msg.Kind = logger.Debug
2546+
}
2547+
2548+
// Attempt to correct an import name with a typo
2549+
repr := nextFile.Repr.(*graph.JSRepr)
2550+
if repr.Meta.ResolvedExportTypos == nil {
2551+
valid := make([]string, 0, len(repr.Meta.ResolvedExports))
2552+
for alias := range repr.Meta.ResolvedExports {
2553+
valid = append(valid, alias)
2554+
}
2555+
sort.Strings(valid)
2556+
typos := helpers.MakeTypoDetector(valid)
2557+
repr.Meta.ResolvedExportTypos = &typos
2558+
}
2559+
if corrected, ok := repr.Meta.ResolvedExportTypos.MaybeCorrectTypo(namedImport.Alias); ok {
2560+
msg.Data.Location.Suggestion = corrected
2561+
export := repr.Meta.ResolvedExports[corrected]
2562+
importedFile := &c.graph.Files[export.SourceIndex]
2563+
msg.Notes = append(msg.Notes, importedFile.LineColumnTracker().MsgData(
2564+
js_lexer.RangeOfIdentifier(importedFile.InputFile.Source, export.NameLoc),
2565+
fmt.Sprintf("Did you mean to import %q instead?", corrected)))
2566+
}
2567+
c.log.AddMsgID(logger.MsgID_Bundler_ImportIsUndefined, msg)
25202568
}
2521-
c.log.AddID(logger.MsgID_Bundler_ImportIsUndefined, kind, trackerFile.LineColumnTracker(), r, fmt.Sprintf(
2522-
"Import %q will always be undefined because there is no matching export in %q",
2523-
namedImport.Alias, c.graph.Files[nextTracker.sourceIndex].InputFile.Source.PrettyPath))
25242569
} else {
25252570
c.log.AddError(trackerFile.LineColumnTracker(), r, fmt.Sprintf("No matching export in %q for import %q",
25262571
c.graph.Files[nextTracker.sourceIndex].InputFile.Source.PrettyPath, namedImport.Alias))

0 commit comments

Comments
 (0)