-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't elide imports when transforming JS files #50404
Changes from 9 commits
1238db9
68021c1
2aa5bfe
0c6f9c7
ef5b062
589eb24
b5c0ec6
07ff4b7
feb0ab7
3b2c300
c2c4fd8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1837,8 +1837,9 @@ namespace ts { | |
nameArg: __String | Identifier | undefined, | ||
isUse: boolean, | ||
excludeGlobals = false, | ||
getSpellingSuggstions = true): Symbol | undefined { | ||
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSpellingSuggstions, getSymbol); | ||
getSpellingSuggestions = true, | ||
reportErrors = true): Symbol | undefined { | ||
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSpellingSuggestions, getSymbol, reportErrors); | ||
} | ||
|
||
function resolveNameHelper( | ||
|
@@ -1850,7 +1851,8 @@ namespace ts { | |
isUse: boolean, | ||
excludeGlobals: boolean, | ||
getSpellingSuggestions: boolean, | ||
lookup: typeof getSymbol): Symbol | undefined { | ||
lookup: typeof getSymbol, | ||
reportErrors = true): Symbol | undefined { | ||
const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location | ||
let result: Symbol | undefined; | ||
let lastLocation: Node | undefined; | ||
|
@@ -2011,7 +2013,9 @@ namespace ts { | |
// TypeScript 1.0 spec (April 2014): 3.4.1 | ||
// The scope of a type parameter extends over the entire declaration with which the type | ||
// parameter list is associated, with the exception of static member declarations in classes. | ||
error(errorLocation, Diagnostics.Static_members_cannot_reference_class_type_parameters); | ||
if (reportErrors) { | ||
error(errorLocation, Diagnostics.Static_members_cannot_reference_class_type_parameters); | ||
} | ||
return undefined; | ||
} | ||
break loop; | ||
|
@@ -2029,7 +2033,7 @@ namespace ts { | |
if (lastLocation === (location as ExpressionWithTypeArguments).expression && (location.parent as HeritageClause).token === SyntaxKind.ExtendsKeyword) { | ||
const container = location.parent.parent; | ||
if (isClassLike(container) && (result = lookup(getSymbolOfNode(container).members!, name, meaning & SymbolFlags.Type))) { | ||
if (nameNotFoundMessage) { | ||
if (reportErrors && nameNotFoundMessage) { | ||
error(errorLocation, Diagnostics.Base_class_expressions_cannot_reference_class_type_parameters); | ||
} | ||
return undefined; | ||
|
@@ -2049,7 +2053,9 @@ namespace ts { | |
if (isClassLike(grandparent) || grandparent.kind === SyntaxKind.InterfaceDeclaration) { | ||
// A reference to this grandparent's type parameters would be an error | ||
if (result = lookup(getSymbolOfNode(grandparent as ClassLikeDeclaration | InterfaceDeclaration).members!, name, meaning & SymbolFlags.Type)) { | ||
error(errorLocation, Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type); | ||
if (reportErrors) { | ||
error(errorLocation, Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type); | ||
} | ||
return undefined; | ||
} | ||
} | ||
|
@@ -2206,7 +2212,7 @@ namespace ts { | |
} | ||
|
||
if (!result) { | ||
if (nameNotFoundMessage) { | ||
if (reportErrors && nameNotFoundMessage) { | ||
addLazyDiagnostic(() => { | ||
if (!errorLocation || | ||
!checkAndReportErrorForMissingPrefix(errorLocation, name, nameArg!) && // TODO: GH#18217 | ||
|
@@ -2259,12 +2265,12 @@ namespace ts { | |
} | ||
return undefined; | ||
} | ||
else if (checkAndReportErrorForInvalidInitializer()) { | ||
else if (reportErrors && checkAndReportErrorForInvalidInitializer()) { | ||
return undefined; | ||
} | ||
|
||
// Perform extra checks only if error reporting was requested | ||
if (nameNotFoundMessage) { | ||
if (reportErrors && nameNotFoundMessage) { | ||
addLazyDiagnostic(() => { | ||
// Only check for block-scoped variable if we have an error location and are looking for the | ||
// name with variable meaning | ||
|
@@ -43070,7 +43076,8 @@ namespace ts { | |
} | ||
const node = getParseTreeNode(nodeIn, isIdentifier); | ||
if (node) { | ||
const symbol = getReferencedValueSymbol(node); | ||
const symbol = getReferencedSymbol(node, /*startInDeclarationContainer*/ undefined); | ||
|
||
// We should only get the declaration of an alias if there isn't a local value | ||
// declaration for the symbol | ||
if (isNonLocalAlias(symbol, /*excludes*/ SymbolFlags.Value) && !getTypeOnlyAliasDeclaration(symbol)) { | ||
|
@@ -43474,6 +43481,41 @@ namespace ts { | |
return resolveName(location, reference.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue | SymbolFlags.Alias, /*nodeNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); | ||
} | ||
|
||
/** | ||
* Get either a value-meaning symbol or an alias symbol. | ||
* Unlike `getReferencedValueSymbol`, if the cached resolved symbol is the unknown symbol, | ||
* we call `resolveName` to find a symbol. | ||
* This is because when caching the resolved symbol, we only consider value symbols, but here | ||
* we want to also get an alias symbol if one exists. | ||
*/ | ||
function getReferencedSymbol(reference: Identifier, startInDeclarationContainer?: boolean): Symbol | undefined { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we know during cache construction whether we'll eventually want alias symbols? Would that interfere with other cache consumers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For (1), I think maybe that could be done. We want aliases for every identifier that goes through a module transform, worst case we need it for every identifier that survived the type erasure transform. But for (2) I think we would need a different cache, because changing it to cache alias-meaning symbols would interfere with the way the checker uses this cache. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not worth the extra complexity of maintaining another cache (conditional on no-emit?), especially before we measure a problem. |
||
const resolvedSymbol = getNodeLinks(reference).resolvedSymbol; | ||
if (resolvedSymbol && resolvedSymbol !== unknownSymbol) { | ||
return resolvedSymbol; | ||
} | ||
|
||
let location: Node = reference; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this new call happen in any scenario that wasn't broken before your change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand the change correctly (dubious), it sounds like the worst case is a file containing only unused imports? If so, could you make such a file with the top 100 npm packages and then measure before and after tsc time? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not unused imports, but identifiers that would resolve to unknown symbol in the cache before, which I think means identifiers that either don't have a value meaning (I guess in JS that would be imports we think refer to types), or identifiers that we couldn't resolve (i.e. imports we couldn't resolve). By file with the top 100 npm packages do you mean a file that imports from the top 100 npm packages? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
From your tests, it looks like you could just
Yes, that's what I meant. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gabritto What does your thumbs-up above mean? "No, it can't happen"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yes, I meant it can happen. But now that I'm thinking about it again, I guess both scenarios I pointed out in my comment above are considered "broken" (both imports that refer to types only and imports we couldn't resolve). So I guess the answer is no. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turns out I was wrong, and we hit the cache for unresolved imports. We don't hit the cache for unresolved identifiers. |
||
if (startInDeclarationContainer) { | ||
// When resolving the name of a declaration as a value, we need to start resolution | ||
// at a point outside of the declaration. | ||
const parent = reference.parent; | ||
if (isDeclaration(parent) && reference === parent.name) { | ||
location = getDeclarationContainer(parent); | ||
} | ||
} | ||
|
||
return resolveName( | ||
location, | ||
reference.escapedText, | ||
SymbolFlags.Value | SymbolFlags.ExportValue | SymbolFlags.Alias, | ||
/*nodeNotFoundMessage*/ undefined, | ||
/*nameArg*/ undefined, | ||
/*isUse*/ true, | ||
/*excludeGlobals*/ undefined, | ||
/*getSpellingSuggestions*/ undefined, | ||
/*reportErrors*/ false); | ||
} | ||
|
||
function getReferencedValueDeclaration(referenceIn: Identifier): Declaration | undefined { | ||
if (!isGeneratedIdentifier(referenceIn)) { | ||
const reference = getParseTreeNode(referenceIn, isIdentifier); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,7 +45,7 @@ b_1["default"]; | |
"use strict"; | ||
exports.__esModule = true; | ||
var x = { x: "" }; | ||
zzz; | ||
a_1["default"]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change looks weird, but it is correct. However, there should be an |
||
var b_1 = require("./b"); | ||
b_1["default"]; | ||
var y = x; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
//// [tests/cases/compiler/elidedJSImport.ts] //// | ||
|
||
//// [caller.js] | ||
import * as fs from 'fs'; | ||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
console.log(fs); | ||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' | ||
|
||
|
||
//// [index.d.ts] | ||
declare module "@truffle/contract" { | ||
// import { ContractObject } from "@truffle/contract-schema"; | ||
interface ContractObject { | ||
foo: number; | ||
} | ||
namespace TruffleContract { | ||
export type Contract = ContractObject; | ||
} | ||
export default TruffleContract; | ||
} | ||
|
||
//// [caller.js] | ||
import * as fs from 'fs'; | ||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
console.log(fs); | ||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
=== tests/cases/compiler/caller.js === | ||
import * as fs from 'fs'; | ||
>fs : Symbol(fs, Decl(caller.js, 0, 6)) | ||
|
||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
>TruffleContract : Symbol(TruffleContract, Decl(caller.js, 1, 6)) | ||
|
||
console.log(fs); | ||
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) | ||
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
>fs : Symbol(fs, Decl(caller.js, 0, 6)) | ||
|
||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' | ||
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) | ||
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
|
||
|
||
=== tests/cases/compiler/node_modules/@truffle/contract/index.d.ts === | ||
declare module "@truffle/contract" { | ||
>"@truffle/contract" : Symbol("@truffle/contract", Decl(index.d.ts, 0, 0)) | ||
|
||
// import { ContractObject } from "@truffle/contract-schema"; | ||
interface ContractObject { | ||
>ContractObject : Symbol(ContractObject, Decl(index.d.ts, 0, 36)) | ||
|
||
foo: number; | ||
>foo : Symbol(ContractObject.foo, Decl(index.d.ts, 2, 30)) | ||
} | ||
namespace TruffleContract { | ||
>TruffleContract : Symbol(TruffleContract, Decl(index.d.ts, 4, 5)) | ||
|
||
export type Contract = ContractObject; | ||
>Contract : Symbol(Contract, Decl(index.d.ts, 5, 31)) | ||
>ContractObject : Symbol(ContractObject, Decl(index.d.ts, 0, 36)) | ||
} | ||
export default TruffleContract; | ||
>TruffleContract : Symbol(TruffleContract, Decl(index.d.ts, 4, 5)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
=== tests/cases/compiler/caller.js === | ||
import * as fs from 'fs'; | ||
>fs : error | ||
|
||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
>TruffleContract : any | ||
|
||
console.log(fs); | ||
>console.log(fs) : void | ||
>console.log : (...data: any[]) => void | ||
>console : Console | ||
>log : (...data: any[]) => void | ||
>fs : error | ||
|
||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' | ||
>console.log('TruffleContract is ', typeof TruffleContract, TruffleContract) : void | ||
>console.log : (...data: any[]) => void | ||
>console : Console | ||
>log : (...data: any[]) => void | ||
>'TruffleContract is ' : "TruffleContract is " | ||
>typeof TruffleContract : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | ||
>TruffleContract : error | ||
>TruffleContract : error | ||
|
||
|
||
=== tests/cases/compiler/node_modules/@truffle/contract/index.d.ts === | ||
declare module "@truffle/contract" { | ||
>"@truffle/contract" : typeof import("@truffle/contract") | ||
|
||
// import { ContractObject } from "@truffle/contract-schema"; | ||
interface ContractObject { | ||
foo: number; | ||
>foo : number | ||
} | ||
namespace TruffleContract { | ||
export type Contract = ContractObject; | ||
>Contract : ContractObject | ||
} | ||
export default TruffleContract; | ||
>TruffleContract : any | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
tests/cases/compiler/caller.js(1,21): error TS2307: Cannot find module 'fs' or its corresponding type declarations. | ||
tests/cases/compiler/caller.js(2,8): error TS18042: 'TruffleContract' is a type and cannot be imported in JavaScript files. Use 'import("@truffle/contract").TruffleContract' in a JSDoc type annotation. | ||
tests/cases/compiler/caller.js(4,43): error TS2708: Cannot use namespace 'TruffleContract' as a value. | ||
tests/cases/compiler/caller.js(4,60): error TS2708: Cannot use namespace 'TruffleContract' as a value. | ||
|
||
|
||
==== tests/cases/compiler/caller.js (4 errors) ==== | ||
import * as fs from 'fs'; | ||
~~~~ | ||
!!! error TS2307: Cannot find module 'fs' or its corresponding type declarations. | ||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
~~~~~~~~~~~~~~~ | ||
!!! error TS18042: 'TruffleContract' is a type and cannot be imported in JavaScript files. Use 'import("@truffle/contract").TruffleContract' in a JSDoc type annotation. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that if |
||
console.log(fs); | ||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' | ||
~~~~~~~~~~~~~~~ | ||
!!! error TS2708: Cannot use namespace 'TruffleContract' as a value. | ||
~~~~~~~~~~~~~~~ | ||
!!! error TS2708: Cannot use namespace 'TruffleContract' as a value. | ||
|
||
|
||
==== tests/cases/compiler/node_modules/@truffle/contract/index.d.ts (0 errors) ==== | ||
declare module "@truffle/contract" { | ||
interface ContractObject { | ||
foo: number; | ||
} | ||
namespace TruffleContract { | ||
export type Contract = ContractObject; | ||
} | ||
export default TruffleContract; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
//// [tests/cases/compiler/elidedJSImport1.ts] //// | ||
|
||
//// [caller.js] | ||
import * as fs from 'fs'; | ||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This import used to be elided, but now it isn't. |
||
console.log(fs); | ||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' | ||
|
||
|
||
//// [index.d.ts] | ||
declare module "@truffle/contract" { | ||
interface ContractObject { | ||
foo: number; | ||
} | ||
namespace TruffleContract { | ||
export type Contract = ContractObject; | ||
} | ||
export default TruffleContract; | ||
} | ||
|
||
//// [caller.js] | ||
import * as fs from 'fs'; | ||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
console.log(fs); | ||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
=== tests/cases/compiler/caller.js === | ||
import * as fs from 'fs'; | ||
>fs : Symbol(fs, Decl(caller.js, 0, 6)) | ||
|
||
import TruffleContract from '@truffle/contract'; // Runtime err: this import is elided in transform | ||
>TruffleContract : Symbol(TruffleContract, Decl(caller.js, 1, 6)) | ||
|
||
console.log(fs); | ||
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) | ||
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
>fs : Symbol(fs, Decl(caller.js, 0, 6)) | ||
|
||
console.log('TruffleContract is ', typeof TruffleContract, TruffleContract); // `TruffleContract` is considered 'unused' | ||
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) | ||
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) | ||
|
||
|
||
=== tests/cases/compiler/node_modules/@truffle/contract/index.d.ts === | ||
declare module "@truffle/contract" { | ||
>"@truffle/contract" : Symbol("@truffle/contract", Decl(index.d.ts, 0, 0)) | ||
|
||
interface ContractObject { | ||
>ContractObject : Symbol(ContractObject, Decl(index.d.ts, 0, 36)) | ||
|
||
foo: number; | ||
>foo : Symbol(ContractObject.foo, Decl(index.d.ts, 1, 30)) | ||
} | ||
namespace TruffleContract { | ||
>TruffleContract : Symbol(TruffleContract, Decl(index.d.ts, 3, 5)) | ||
|
||
export type Contract = ContractObject; | ||
>Contract : Symbol(Contract, Decl(index.d.ts, 4, 31)) | ||
>ContractObject : Symbol(ContractObject, Decl(index.d.ts, 0, 36)) | ||
} | ||
export default TruffleContract; | ||
>TruffleContract : Symbol(TruffleContract, Decl(index.d.ts, 3, 5)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolveNameHelper
used to report errors even ifnameNotFoundMessage
wasundefined
. Since we now callresolveNameHelper
ingetReferencedValueOrAliasSymbol
when skipping the cached resolved symbol, without this change we would get different number of pre-emit and post-emit reported errors, and they'd be useless/repeated errors anyways.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolveEntityName
already takes anignoreErrors
flag and is seemingly what we use to resolve type names elsewhere - it just passes anundefined
nameNotFoundMessage
in those cases. Instead of adding a new parameter that also needs to be passed in at theresolveEntityName
and maybe other callsites, maybe checking the existingnameNotFoundMessage
parameter in more places works?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the existing code in the checker somehow relying on the fact that we report some errors even when
nameNotFoundMessage
isundefined
? I thought this was intentional and the check for an undefinednameNotFoundMessage
just kept us from reporting some "not found" errors.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably not. I'd change it and see if it breaks anything.