-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Import assertion #40698
Import assertion #40698
Conversation
f945374
to
e1b3b78
Compare
@typescript-bot pack this. |
src/compiler/checker.ts
Outdated
function checkGrammarImportAssertion(declaration: ImportDeclaration | ExportDeclaration) { | ||
const target = getEmitScriptTarget(compilerOptions); | ||
if (target < ScriptTarget.ESNext && declaration.assertClause) { | ||
grammarErrorOnNode(declaration.assertClause, Diagnostics.Import_assertions_are_not_available_when_targeting_lower_than_esnext); |
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.
According to the current specification of Import Assertion, the import clause cannot affect the representation of the source text. Does it mean it can be safely ignored (when downlevel, drop the whole assert clause instead of emitting an error)?
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.
Well. IMO, downlevel means something could map to another. Ignore is not a good idea.
But maybe we could control the error by module options rather than target.
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.
Import Assertions are an enrichment to the import statement, provides the runtime with extra information of which it can make a decision about. I could see the need to allow an error by default, but allow the error to be ignored and the node to be dropped from the emit.
The biggest use case is supporting JSON modules, and there are several runtimes that already support JSON modules without the assertion, but I can see people wanting to start writing the assertions in the code for the runtimes that will only support it with the assertion clause.
The TypeScript team hasn't accepted the linked issue #40694. If you can get it accepted, this PR will have a better chance of being reviewed. |
uP. |
Up. |
Sorry for the delay @Kingwl. I'm looking at this again today. |
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.
FYI, I will be OOF through Tuesday and may not be able to respond to feedback until Wednesday.
src/compiler/checker.ts
Outdated
// see: parseArgumentOrArrayLiteralElement...we use this function which parse arguments of callExpression to parse specifier for dynamic import. | ||
// parseArgumentOrArrayLiteralElement allows spread element to be in an argument list which is not allowed as specifier in dynamic import. | ||
if (isSpreadElement(nodeArguments[0])) { | ||
if (nodeArguments.length && isSpreadElement(nodeArguments[0])) { |
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.
Do we need to check that all elements of the assert
property of the second argument have type string
? Per https://tc39.es/proposal-import-assertions/#sec-evaluate-import-call, Step 10.d.v.3:
No coercion to string is performed here, the algorithm explicitly checks for strings.
We might want to consider something like the ImportMeta
interface for the import argument, something like:
// es5.d.ts
interface ImportCallOptions {
assert?: ImportAssertions;
}
interface ImportAssertions {
type?: string;
[key: string]: string | undefined; // not ideal, but necessary so that `type` is optional.
}
Then we can check against the global ImportCallOptions
type, and if other assertions or options are added later, they can be introduced using global augmentation similar to ImportMeta
.
We could consider this as a follow-on PR if necessary, but it would be nice to have.
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.
NOTE: if we wanted to be more specific with type
we'd need to create an open-ended union via an interface as well:
interface ImportAssertions {
type?: ImportTypeAssertion;
[key: string]: string | undefined;
}
// see ArrayBufferTypes for reference
interface ImportTypeAssertionTypes {
json: "json";
}
type ImportTypeAssertion = Extract<ImportTypeAssertions[keyof ImportTypeAssertions], string>;
That way, we have better completions for type
and if a host environment supported other values for type
, they could also be added via augmentation, i.e.:
interface ImportTypeAssertionTypes {
css: "css";
}
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.
Also, the nodeArguments.length
check shouldn't be necessary if checkGrammarImportCallArguments
already asserts that nodeArguments.length
can't be zero.
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.
We also need to ensure that nodeArguments[1]
(if present) is not a spread element.
I would suggest this approach:
const spreadElement = forEach(nodeArguments, isSpreadElement);
if (spreadElement) {
return grammarErrorOnNode(spreadElement, Diagnostics.Specifier_of_dynamic_import_cannot_be_spread_element);
}
That way it checks all arguments, but only reports on the first error (which is almost always the case for grammar 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.
Should we check the assert clause too?Or just the second parameter of dynamic import.
tests/baselines/reference/importAssertion1(module=esnext).errors.txt
Outdated
Show resolved
Hide resolved
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.
A few more minor changes. @DanielRosenwasser, @andrewbranch do we want to add something like #40698 (comment) in this PR or in a follow up?
src/compiler/checker.ts
Outdated
|
||
if (node.typeArguments) { | ||
return grammarErrorOnNode(node, Diagnostics.Dynamic_import_cannot_have_type_arguments); | ||
if (nodeArguments.length === 0 || nodeArguments.length > 2) { |
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.
Nit: remove extra space
if (nodeArguments.length === 0 || nodeArguments.length > 2) { | |
if (nodeArguments.length === 0 || nodeArguments.length > 2) { |
src/compiler/checker.ts
Outdated
const spreadElement = forEach(nodeArguments, isSpreadElement); | ||
if (spreadElement) { | ||
return grammarErrorOnNode(nodeArguments[0], Diagnostics.Specifier_of_dynamic_import_cannot_be_spread_element); | ||
} |
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.
Hmm. This doesn't quite do what we want. We want to report the error on the spread element, not the first argument (which might not be the spread element). I think we want this instead:
const spreadElement = forEach(nodeArguments, isSpreadElement); | |
if (spreadElement) { | |
return grammarErrorOnNode(nodeArguments[0], Diagnostics.Specifier_of_dynamic_import_cannot_be_spread_element); | |
} | |
const spreadElement = find(nodeArguments, isSpreadElement); | |
if (spreadElement) { | |
return grammarErrorOnNode(spreadElement, Diagnostics.Specifier_of_dynamic_import_cannot_be_spread_element); | |
} | |
src/compiler/types.ts
Outdated
createImportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression, assertClause: AssertClause | undefined): ImportDeclaration; | ||
updateImportDeclaration(node: ImportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression, assertClause: AssertClause | undefined): ImportDeclaration; | ||
createImportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression, assertClause?: AssertClause): ImportDeclaration; | ||
updateImportDeclaration(node: ImportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression, assertClause?: AssertClause): ImportDeclaration; |
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.
I would leave | undefined
here instead of adding ?
. My previous comment was specific to the create
. For a create
, there's no sense breaking existing code because they weren't previously passing an argument. For an update
, we do want to break existing code because there's now a new node they may not be appropriately handling.
src/compiler/types.ts
Outdated
@@ -7360,7 +7360,7 @@ namespace ts { | |||
createExportAssignment(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression): ExportAssignment; | |||
updateExportAssignment(node: ExportAssignment, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, expression: Expression): ExportAssignment; | |||
createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression, assertClause?: AssertClause): ExportDeclaration; | |||
updateExportDeclaration(node: ExportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier: Expression | undefined, assertClause: AssertClause | undefined): ExportDeclaration; | |||
updateExportDeclaration(node: ExportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isTypeOnly: boolean, exportClause: NamedExportBindings | undefined, moduleSpecifier: Expression | undefined, assertClause?: AssertClause): ExportDeclaration; |
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.
I also would leave | undefined
here. We want to break callers here since they could be forgetting to handle a node they weren't previously handling.
src/compiler/diagnosticMessages.json
Outdated
@@ -1388,6 +1388,10 @@ | |||
"category": "Message", | |||
"code": 1449 | |||
}, | |||
"Dynamic import must only have a specifier and an optional assertion as arguments": { |
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.
"Dynamic import must only have a specifier and an optional assertion as arguments": { | |
"Dynamic imports can only accept a path and an optional assertion as arguments": { |
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.
Agree, but I think module specifier
is a more correct term than path
src/compiler/diagnosticMessages.json
Outdated
@@ -924,7 +924,7 @@ | |||
"category": "Error", | |||
"code": 1323 | |||
}, | |||
"Dynamic import must have one specifier as an argument.": { | |||
"Dynamic import only supports a second argument when the '--module' option is set to 'esnext'.": { |
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.
"Dynamic import only supports a second argument when the '--module' option is set to 'esnext'.": { | |
"Dynamic imports only support a second argument when the '--module' option is set to 'esnext'.": { |
Let’s try to get this in this PR if possible. Preventing runtime errors seems high priority to me. I personally like the idea of an open-ended interface, but I think that warrants some design discussion to make sure we’re all on the same page. Let’s discuss at Friday’s meeting. |
@Kingwl, if you don't mind I'm going to make some of these minor changes and add a basic implementation of #40698 (comment) that we can expand on later. |
I found a small bug in program.ts related to |
# Conflicts: # src/compiler/diagnosticMessages.json
Never mind. Thanks for your changes. |
I fixed completions for |
9accee9
to
b07c6e9
Compare
Thanks for all the effort you put into this @Kingwl! |
Fixes #40694