Skip to content
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

Merged
merged 37 commits into from
Sep 20, 2021
Merged

Import assertion #40698

merged 37 commits into from
Sep 20, 2021

Conversation

Kingwl
Copy link
Contributor

@Kingwl Kingwl commented Sep 22, 2020

Fixes #40694

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Sep 22, 2020
@Kingwl
Copy link
Contributor Author

Kingwl commented Sep 22, 2020

@typescript-bot pack this.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 22, 2020

Heya @Kingwl, I've started to run the tarball bundle task on this PR at 375b433. You can monitor the build here.

@Kingwl Kingwl marked this pull request as ready for review September 22, 2020 16:59
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);
Copy link
Contributor

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)?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@typescript-bot typescript-bot added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Sep 23, 2020
@sandersn sandersn added this to Not started in PR Backlog Oct 2, 2020
@sandersn sandersn moved this from Not started to Needs review in PR Backlog Oct 5, 2020
@typescript-bot
Copy link
Collaborator

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.

@Kingwl
Copy link
Contributor Author

Kingwl commented Dec 31, 2020

uP.

src/compiler/checker.ts Outdated Show resolved Hide resolved
@Kingwl
Copy link
Contributor Author

Kingwl commented Sep 8, 2021

Up.

@rbuckton
Copy link
Member

rbuckton commented Sep 9, 2021

Sorry for the delay @Kingwl. I'm looking at this again today.

Copy link
Member

@rbuckton rbuckton left a 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 Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/emitter.ts Outdated Show resolved Hide resolved
src/compiler/emitter.ts Outdated Show resolved Hide resolved
src/compiler/utilitiesPublic.ts Outdated Show resolved Hide resolved
src/compiler/types.ts Outdated Show resolved Hide resolved
// 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])) {
Copy link
Member

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:

image

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.

Copy link
Member

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";
}

Copy link
Member

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.

Copy link
Member

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).

Copy link
Contributor Author

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.

src/compiler/checker.ts Outdated Show resolved Hide resolved
Copy link
Member

@rbuckton rbuckton left a 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?


if (node.typeArguments) {
return grammarErrorOnNode(node, Diagnostics.Dynamic_import_cannot_have_type_arguments);
if (nodeArguments.length === 0 || nodeArguments.length > 2) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: remove extra space

Suggested change
if (nodeArguments.length === 0 || nodeArguments.length > 2) {
if (nodeArguments.length === 0 || nodeArguments.length > 2) {

Comment on lines 43191 to 43194
const spreadElement = forEach(nodeArguments, isSpreadElement);
if (spreadElement) {
return grammarErrorOnNode(nodeArguments[0], Diagnostics.Specifier_of_dynamic_import_cannot_be_spread_element);
}
Copy link
Member

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:

Suggested change
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);
}

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;
Copy link
Member

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.

@@ -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;
Copy link
Member

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.

@@ -1388,6 +1388,10 @@
"category": "Message",
"code": 1449
},
"Dynamic import must only have a specifier and an optional assertion as arguments": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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": {

Copy link
Member

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

@@ -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'.": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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'.": {

@andrewbranch
Copy link
Member

Do we need to check that all elements of the assert property of the second argument have type string?

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.

@rbuckton
Copy link
Member

@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.

@rbuckton
Copy link
Member

I found a small bug in program.ts related to import("./module", {}) in that we aren't resolving ./module because the argument count isn't 1. I'll push changes up shortly. It is definitely strange to not get completions at | in import("./module", { | }), and not getting a signature help popup.

@Kingwl
Copy link
Contributor Author

Kingwl commented Sep 18, 2021

Never mind. Thanks for your changes.

@rbuckton
Copy link
Member

I fixed completions for import("path", { | }), but we still don't handle completions in import ... assert { }. This is probably fine, since we've typed ImportAssertions to just [key: string]: string so you wouldn't really get useful completions right now anyways. If we decide to make type a property of ImportAssertions in a later PR, we'll want to re-investigate completions for import/export with assert.

PR Backlog automation moved this from Waiting on author to Needs merge Sep 20, 2021
@rbuckton rbuckton merged commit ec114b8 into microsoft:main Sep 20, 2021
PR Backlog automation moved this from Needs merge to Done Sep 20, 2021
@rbuckton
Copy link
Member

Thanks for all the effort you put into this @Kingwl!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project
PR Backlog
  
Done
Development

Successfully merging this pull request may close these issues.

Implement Import Assertions (stage 3)
7 participants