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

Implement "Arbitrary Module Namespace Identifiers" #58640

Merged
merged 13 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ import {
ModifierFlags,
ModuleBlock,
ModuleDeclaration,
moduleExportNameIsDefault,
Mutable,
NamespaceExportDeclaration,
Node,
Expand Down Expand Up @@ -433,6 +434,9 @@ function getModuleInstanceStateWorker(node: Node, visited: Map<number, ModuleIns

function getModuleInstanceStateForAliasTarget(specifier: ExportSpecifier, visited: Map<number, ModuleInstanceState | undefined>) {
const name = specifier.propertyName || specifier.name;
if (name.kind !== SyntaxKind.Identifier) {
return ModuleInstanceState.Instantiated; // Skip for invalid syntax like this: export { "x" }
}
let p: Node | undefined = specifier.parent;
while (p) {
if (isBlock(p) || isModuleBlock(p) || isSourceFile(p)) {
Expand Down Expand Up @@ -759,7 +763,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
function declareSymbol(symbolTable: SymbolTable, parent: Symbol | undefined, node: Declaration, includes: SymbolFlags, excludes: SymbolFlags, isReplaceableByMethod?: boolean, isComputedName?: boolean): Symbol {
Debug.assert(isComputedName || !hasDynamicName(node));

const isDefaultExport = hasSyntacticModifier(node, ModifierFlags.Default) || isExportSpecifier(node) && node.name.escapedText === "default";
const isDefaultExport = hasSyntacticModifier(node, ModifierFlags.Default) || isExportSpecifier(node) && moduleExportNameIsDefault(node.name);

// The exported symbol for an export default function/class node is always named "default"
const name = isComputedName ? InternalSymbolName.Computed
Expand Down
131 changes: 87 additions & 44 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -8325,5 +8325,9 @@
"Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.": {
"category": "Error",
"code": 18056
},
"String literal import and export names are not supported when the '--module' flag is set to 'es2015' or 'es2020'.": {
"category": "Error",
"code": 18057
}
}
13 changes: 7 additions & 6 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ import {
ModuleBlock,
ModuleBody,
ModuleDeclaration,
ModuleExportName,
ModuleName,
ModuleReference,
Mutable,
Expand Down Expand Up @@ -4842,7 +4843,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createNamespaceExport(name: Identifier): NamespaceExport {
function createNamespaceExport(name: ModuleExportName): NamespaceExport {
const node = createBaseDeclaration<NamespaceExport>(SyntaxKind.NamespaceExport);
node.name = name;
node.transformFlags |= propagateChildFlags(node.name) |
Expand All @@ -4852,7 +4853,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function updateNamespaceExport(node: NamespaceExport, name: Identifier) {
function updateNamespaceExport(node: NamespaceExport, name: ModuleExportName) {
return node.name !== name
? update(createNamespaceExport(name), node)
: node;
Expand All @@ -4875,7 +4876,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createImportSpecifier(isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) {
function createImportSpecifier(isTypeOnly: boolean, propertyName: ModuleExportName | undefined, name: Identifier) {
const node = createBaseDeclaration<ImportSpecifier>(SyntaxKind.ImportSpecifier);
node.isTypeOnly = isTypeOnly;
node.propertyName = propertyName;
Expand All @@ -4887,7 +4888,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function updateImportSpecifier(node: ImportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) {
function updateImportSpecifier(node: ImportSpecifier, isTypeOnly: boolean, propertyName: ModuleExportName | undefined, name: Identifier) {
return node.isTypeOnly !== isTypeOnly
|| node.propertyName !== propertyName
|| node.name !== name
Expand Down Expand Up @@ -4994,7 +4995,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createExportSpecifier(isTypeOnly: boolean, propertyName: string | Identifier | undefined, name: string | Identifier) {
function createExportSpecifier(isTypeOnly: boolean, propertyName: string | ModuleExportName | undefined, name: string | ModuleExportName) {
const node = createBaseNode<ExportSpecifier>(SyntaxKind.ExportSpecifier);
node.isTypeOnly = isTypeOnly;
node.propertyName = asName(propertyName);
Expand All @@ -5008,7 +5009,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function updateExportSpecifier(node: ExportSpecifier, isTypeOnly: boolean, propertyName: Identifier | undefined, name: Identifier) {
function updateExportSpecifier(node: ExportSpecifier, isTypeOnly: boolean, propertyName: ModuleExportName | undefined, name: ModuleExportName) {
return node.isTypeOnly !== isTypeOnly
|| node.propertyName !== propertyName
|| node.name !== name
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ import {
MissingDeclaration,
ModuleBlock,
ModuleDeclaration,
ModuleExportName,
NamedExports,
NamedImports,
NamedTupleMember,
Expand Down Expand Up @@ -898,6 +899,10 @@ export function isExportSpecifier(node: Node): node is ExportSpecifier {
return node.kind === SyntaxKind.ExportSpecifier;
}

export function isModuleExportName(node: Node): node is ModuleExportName {
return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.StringLiteral;
}

export function isMissingDeclaration(node: Node): node is MissingDeclaration {
return node.kind === SyntaxKind.MissingDeclaration;
}
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/factory/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,9 @@ export function getLocalNameForExternalImport(factory: NodeFactory, node: Import
const namespaceDeclaration = getNamespaceDeclarationNode(node);
if (namespaceDeclaration && !isDefaultImport(node) && !isExportNamespaceAsDefaultDeclaration(node)) {
const name = namespaceDeclaration.name;
if (name.kind === SyntaxKind.StringLiteral) {
return factory.getGeneratedNameForNode(node);
}
return isGeneratedIdentifier(name) ? name : factory.createIdentifier(getSourceTextOfNodeFromSourceFile(sourceFile, name) || idText(name));
}
if (node.kind === SyntaxKind.ImportDeclaration && node.importClause) {
Expand Down
56 changes: 39 additions & 17 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ import {
modifiersToFlags,
ModuleBlock,
ModuleDeclaration,
ModuleExportName,
ModuleKind,
Mutable,
NamedExportBindings,
Expand Down Expand Up @@ -2906,6 +2907,9 @@ namespace Parser {
if (token() === SyntaxKind.FromKeyword && lookAhead(nextTokenIsStringLiteral)) {
return false;
}
if (token() === SyntaxKind.StringLiteral) {
return true; // For "arbitrary module namespace identifiers"
}
return tokenIsIdentifierOrKeyword(token());
case ParsingContext.JsxAttributes:
return tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.OpenBraceToken;
Expand Down Expand Up @@ -8504,6 +8508,14 @@ namespace Parser {
return finishNode(factory.createNamespaceImport(name), pos);
}

function canParseModuleExportName(): boolean {
return tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.StringLiteral;
}

function parseModuleExportName(parseName: () => Identifier): ModuleExportName {
return token() === SyntaxKind.StringLiteral ? parseLiteralNode() as StringLiteral : parseName();
}

function parseNamedImportsOrExports(kind: SyntaxKind.NamedImports): NamedImports;
function parseNamedImportsOrExports(kind: SyntaxKind.NamedExports): NamedExports;
function parseNamedImportsOrExports(kind: SyntaxKind): NamedImportsOrExports {
Expand Down Expand Up @@ -8536,18 +8548,18 @@ namespace Parser {
const pos = getNodePos();
// ImportSpecifier:
// BindingIdentifier
// IdentifierName as BindingIdentifier
// ModuleExportName as BindingIdentifier
// ExportSpecifier:
// IdentifierName
// IdentifierName as IdentifierName
// ModuleExportName
// ModuleExportName as ModuleExportName
let checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier();
let checkIdentifierStart = scanner.getTokenStart();
let checkIdentifierEnd = scanner.getTokenEnd();
let isTypeOnly = false;
let propertyName: Identifier | undefined;
let propertyName: ModuleExportName | undefined;
let canParseAsKeyword = true;
let name = parseIdentifierName();
if (name.escapedText === "type") {
let name = parseModuleExportName(parseIdentifierName);
if (name.kind === SyntaxKind.Identifier && name.escapedText === "type") {
// If the first token of an import specifier is 'type', there are a lot of possibilities,
// especially if we see 'as' afterwards:
//
Expand All @@ -8561,11 +8573,12 @@ namespace Parser {
if (token() === SyntaxKind.AsKeyword) {
// { type as as ...? }
const secondAs = parseIdentifierName();
if (tokenIsIdentifierOrKeyword(token())) {
if (canParseModuleExportName()) {
// { type as as something }
// { type as as "something" }
isTypeOnly = true;
propertyName = firstAs;
name = parseNameWithKeywordCheck();
name = parseModuleExportName(parseNameWithKeywordCheck);
canParseAsKeyword = false;
}
else {
Expand All @@ -8575,35 +8588,44 @@ namespace Parser {
canParseAsKeyword = false;
}
}
else if (tokenIsIdentifierOrKeyword(token())) {
else if (canParseModuleExportName()) {
// { type as something }
// { type as "something" }
propertyName = name;
canParseAsKeyword = false;
name = parseNameWithKeywordCheck();
name = parseModuleExportName(parseNameWithKeywordCheck);
}
else {
// { type as }
isTypeOnly = true;
name = firstAs;
}
}
else if (tokenIsIdentifierOrKeyword(token())) {
else if (canParseModuleExportName()) {
// { type something ...? }
// { type "something" ...? }
isTypeOnly = true;
name = parseNameWithKeywordCheck();
name = parseModuleExportName(parseNameWithKeywordCheck);
}
}

if (canParseAsKeyword && token() === SyntaxKind.AsKeyword) {
propertyName = name;
parseExpected(SyntaxKind.AsKeyword);
name = parseNameWithKeywordCheck();
name = parseModuleExportName(parseNameWithKeywordCheck);
}
if (kind === SyntaxKind.ImportSpecifier && checkIdentifierIsKeyword) {
parseErrorAt(checkIdentifierStart, checkIdentifierEnd, Diagnostics.Identifier_expected);
if (kind === SyntaxKind.ImportSpecifier) {
if (name.kind !== SyntaxKind.Identifier) {
// ImportSpecifier casts "name" to Identifier below, so make sure it's an identifier
parseErrorAt(skipTrivia(sourceText, name.pos), name.end, Diagnostics.Identifier_expected);
name = setTextRangePosEnd(createMissingNode<Identifier>(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false), name.pos, name.pos);
}
else if (checkIdentifierIsKeyword) {
parseErrorAt(checkIdentifierStart, checkIdentifierEnd, Diagnostics.Identifier_expected);
}
}
const node = kind === SyntaxKind.ImportSpecifier
? factory.createImportSpecifier(isTypeOnly, propertyName, name)
? factory.createImportSpecifier(isTypeOnly, propertyName, name as Identifier)
evanw marked this conversation as resolved.
Show resolved Hide resolved
: factory.createExportSpecifier(isTypeOnly, propertyName, name);
return finishNode(node, pos);

Expand All @@ -8616,7 +8638,7 @@ namespace Parser {
}

function parseNamespaceExport(pos: number): NamespaceExport {
return finishNode(factory.createNamespaceExport(parseIdentifierName()), pos);
return finishNode(factory.createNamespaceExport(parseModuleExportName(parseIdentifierName)), pos);
}

function parseExportDeclaration(pos: number, hasJSDoc: boolean, modifiers: NodeArray<ModifierLike> | undefined): ExportDeclaration {
Expand Down
Loading