Skip to content

Commit

Permalink
Implement "Arbitrary Module Namespace Identifiers" (#58640)
Browse files Browse the repository at this point in the history
Co-authored-by: Ron Buckton <ron.buckton@microsoft.com>
  • Loading branch information
evanw and rbuckton committed Jun 4, 2024
1 parent 112e860 commit 8d62e2f
Show file tree
Hide file tree
Showing 93 changed files with 9,016 additions and 186 deletions.
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)
: 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

0 comments on commit 8d62e2f

Please sign in to comment.