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 5 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
122 changes: 79 additions & 43 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 @@ -8317,5 +8317,9 @@
"Enum member following a non-literal numeric member must have an initializer when 'isolatedModules' is enabled.": {
"category": "Error",
"code": 18056
},
"Arbitrary module namespace identifiers are not supported when the '--module' flag is set to 'es2015' or 'es2020'.": {
evanw marked this conversation as resolved.
Show resolved Hide resolved
"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
55 changes: 45 additions & 10 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ import {
modifiersToFlags,
ModuleBlock,
ModuleDeclaration,
ModuleExportName,
ModuleKind,
Mutable,
NamedExportBindings,
Expand Down Expand Up @@ -2911,6 +2912,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 @@ -8509,6 +8513,10 @@ namespace Parser {
return finishNode(factory.createNamespaceImport(name), pos);
}

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 @@ -8541,18 +8549,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 Down Expand Up @@ -8592,23 +8600,28 @@ namespace Parser {
name = firstAs;
}
}
else if (token() === SyntaxKind.StringLiteral) {
// { type "something" as ... }
isTypeOnly = true;
name = parseLiteralNode() as StringLiteral;
}
else if (tokenIsIdentifierOrKeyword(token())) {
// { type something ...? }
isTypeOnly = true;
name = parseNameWithKeywordCheck();
}
}

if (canParseAsKeyword && token() === SyntaxKind.AsKeyword) {
if ((kind === SyntaxKind.ImportSpecifier && name.kind === SyntaxKind.StringLiteral) || (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);
}
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 @@ -8621,7 +8634,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 Expand Up @@ -8653,6 +8666,28 @@ namespace Parser {
if (moduleSpecifier && (currentToken === SyntaxKind.WithKeyword || currentToken === SyntaxKind.AssertKeyword) && !scanner.hasPrecedingLineBreak()) {
attributes = parseImportAttributes(currentToken);
}

// String literals are only allowed in the export specifiers if there's
// a module present:
//
// // Valid:
// export { "x" } from "foo";
// export { "x" as y } from "foo";
// export { x as "y" };
//
// // Invalid:
// export { "x" };
// export { "x" as y };
//
if (exportClause && exportClause.kind === SyntaxKind.NamedExports && !moduleSpecifier) {
evanw marked this conversation as resolved.
Show resolved Hide resolved
for (const element of exportClause.elements) {
const name = element.propertyName || element.name;
if (name.kind === SyntaxKind.StringLiteral) {
parseErrorAt(skipTrivia(sourceText, name.pos), name.end, Diagnostics.Identifier_expected);
}
}
}

parseSemicolon();
setAwaitContext(savedAwaitContext);
const node = factory.createExportDeclaration(modifiers, isTypeOnly, exportClause, moduleSpecifier, attributes);
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,30 @@ export function isIdentifierText(name: string, languageVersion: ScriptTarget | u
return true;
}

/** @internal */
export function generateIdentifierForArbitraryString(text: string, languageVersion: ScriptTarget | undefined): string {
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason this is in scanner.ts? It seems to only be used in services so there's no reason it should need to be loaded by tsc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's here because it's written using charSize like other identifier-related code, but charSize is not currently exported. I'll move this into completions.ts and inline charSize since it's a very small function.

let needsUnderscore = false;
let identifier = "";
let ch: number;

// Convert "(example, text)" into "_example_text_"
for (let i = 0; i < text.length; i += charSize(ch)) {
ch = codePointAt(text, i);
if (i === 0 ? isIdentifierStart(ch, languageVersion) : isIdentifierPart(ch, languageVersion)) {
if (needsUnderscore) identifier += "_";
identifier += String.fromCodePoint(ch);
needsUnderscore = false;
}
else {
needsUnderscore = true;
}
}
if (needsUnderscore) identifier += "_";

// Default to "_" if the provided text was empty
return identifier || "_";
}

const enum ClassSetExpressionType {
Unknown,
ClassUnion,
Expand Down
Loading