Skip to content

Commit

Permalink
feat(41825): JSDoc equivalent of import * (#57207)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk committed Mar 26, 2024
1 parent a22aaf0 commit 2de69b0
Show file tree
Hide file tree
Showing 116 changed files with 2,689 additions and 91 deletions.
47 changes: 47 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import {
getExpandoInitializer,
getHostSignatureFromJSDoc,
getImmediatelyInvokedFunctionExpression,
getJSDocHost,
getJSDocTypeTag,
getLeftmostAccessExpression,
getNameOfDeclaration,
Expand Down Expand Up @@ -229,6 +230,7 @@ import {
JSDocClassTag,
JSDocEnumTag,
JSDocFunctionType,
JSDocImportTag,
JSDocOverloadTag,
JSDocParameterTag,
JSDocPropertyLikeTag,
Expand Down Expand Up @@ -525,6 +527,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
var lastContainer: HasLocals;
var delayedTypeAliases: (JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag)[];
var seenThisKeyword: boolean;
var jsDocImports: JSDocImportTag[];

// state used by control flow analysis
var currentFlow: FlowNode;
Expand Down Expand Up @@ -592,6 +595,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
file.symbolCount = symbolCount;
file.classifiableNames = classifiableNames;
delayedBindJSDocTypedefTag();
bindJSDocImports();
}

file = undefined!;
Expand All @@ -603,6 +607,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
blockScopeContainer = undefined!;
lastContainer = undefined!;
delayedTypeAliases = undefined!;
jsDocImports = undefined!;
seenThisKeyword = false;
currentFlow = undefined!;
currentBreakTarget = undefined;
Expand Down Expand Up @@ -1181,6 +1186,9 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
bindJSDocTypeAlias(node as JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag);
break;
// In source files and blocks, bind functions first to match hoisting that occurs at runtime
case SyntaxKind.JSDocImportTag:
bindJSDocImportTag(node as JSDocImportTag);
break;
case SyntaxKind.SourceFile: {
bindEachFunctionsFirst((node as SourceFile).statements);
bind((node as SourceFile).endOfFileToken);
Expand Down Expand Up @@ -2065,6 +2073,14 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
}
}

function bindJSDocImportTag(node: JSDocImportTag) {
bind(node.tagName);

if (typeof node.comment !== "string") {
bindEach(node.comment);
}
}

function bindOptionalExpression(node: Expression, trueTarget: FlowLabel, falseTarget: FlowLabel) {
doWithConditionalBranches(bind, node, trueTarget, falseTarget);
if (!isOptionalChain(node) || isOutermostOptionalChain(node)) {
Expand Down Expand Up @@ -2443,6 +2459,35 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
currentFlow = saveCurrentFlow;
}

function bindJSDocImports() {
if (jsDocImports === undefined) {
return;
}

const saveContainer = container;
const saveLastContainer = lastContainer;
const saveBlockScopeContainer = blockScopeContainer;
const saveParent = parent;
const saveCurrentFlow = currentFlow;

for (const jsDocImportTag of jsDocImports) {
const host = getJSDocHost(jsDocImportTag);
const enclosingContainer = host ? getEnclosingContainer(host) as IsContainer | undefined : undefined;
const enclosingBlockScopeContainer = host ? getEnclosingBlockScopeContainer(host) as IsBlockScopedContainer | undefined : undefined;
container = enclosingContainer || file;
blockScopeContainer = enclosingBlockScopeContainer || file;
currentFlow = initFlowNode({ flags: FlowFlags.Start });
parent = jsDocImportTag;
bind(jsDocImportTag.importClause);
}

container = saveContainer;
lastContainer = saveLastContainer;
blockScopeContainer = saveBlockScopeContainer;
parent = saveParent;
currentFlow = saveCurrentFlow;
}

// The binder visits every node in the syntax tree so it is a convenient place to perform a single localized
// check for reserved words used as identifiers in strict mode code, as well as `yield` or `await` in
// [Yield] or [Await] contexts, respectively.
Expand Down Expand Up @@ -2995,6 +3040,8 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
return (delayedTypeAliases || (delayedTypeAliases = [])).push(node as JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag);
case SyntaxKind.JSDocOverloadTag:
return bind((node as JSDocOverloadTag).typeExpression);
case SyntaxKind.JSDocImportTag:
return (jsDocImports || (jsDocImports = [])).push(node as JSDocImportTag);
}
}

Expand Down
36 changes: 26 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
AmbientModuleDeclaration,
and,
AnonymousType,
AnyImportOrJsDocImport,
AnyImportOrReExport,
AnyImportSyntax,
append,
appendIfUnique,
ArrayBindingPattern,
Expand Down Expand Up @@ -581,6 +581,7 @@ import {
isJSDocCallbackTag,
isJSDocConstructSignature,
isJSDocFunctionType,
isJSDocImportTag,
isJSDocIndexSignature,
isJSDocLinkLike,
isJSDocMemberName,
Expand Down Expand Up @@ -770,6 +771,7 @@ import {
JSDocEnumTag,
JSDocFunctionType,
JSDocImplementsTag,
JSDocImportTag,
JSDocLink,
JSDocLinkCode,
JSDocLinkPlain,
Expand Down Expand Up @@ -3382,6 +3384,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.JSDocTypedefTag:
case SyntaxKind.JSDocCallbackTag:
case SyntaxKind.JSDocEnumTag:
case SyntaxKind.JSDocImportTag:
// js type aliases do not resolve names from their host, so skip past it
const root = getJSDocRoot(location);
if (root) {
Expand Down Expand Up @@ -3950,7 +3953,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
|| (n === stopAt || isFunctionLike(n) && (!getImmediatelyInvokedFunctionExpression(n) || (getFunctionFlags(n) & FunctionFlags.AsyncGenerator)) ? "quit" : false));
}

function getAnyImportSyntax(node: Node): AnyImportSyntax | undefined {
function getAnyImportSyntax(node: Node): AnyImportOrJsDocImport | undefined {
switch (node.kind) {
case SyntaxKind.ImportEqualsDeclaration:
return node as ImportEqualsDeclaration;
Expand Down Expand Up @@ -4288,8 +4291,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function getExternalModuleMember(node: ImportDeclaration | ExportDeclaration | VariableDeclaration, specifier: ImportOrExportSpecifier | BindingElement | PropertyAccessExpression, dontResolveAlias = false): Symbol | undefined {
const moduleSpecifier = getExternalModuleRequireArgument(node) || (node as ImportDeclaration | ExportDeclaration).moduleSpecifier!;
function getExternalModuleMember(node: ImportDeclaration | ExportDeclaration | VariableDeclaration | JSDocImportTag, specifier: ImportOrExportSpecifier | BindingElement | PropertyAccessExpression, dontResolveAlias = false): Symbol | undefined {
const moduleSpecifier = getExternalModuleRequireArgument(node) || (node as ImportDeclaration | ExportDeclaration | JSDocImportTag).moduleSpecifier!;
const moduleSymbol = resolveExternalModuleName(node, moduleSpecifier)!; // TODO: GH#18217
const name = !isPropertyAccessExpression(specifier) && specifier.propertyName || specifier.name;
if (!isIdentifier(name)) {
Expand Down Expand Up @@ -5014,6 +5017,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
? location
: (isModuleDeclaration(location) ? location : location.parent && isModuleDeclaration(location.parent) && location.parent.name === location ? location.parent : undefined)?.name ||
(isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal ||
(isInJSFile(location) && isJSDocImportTag(location) ? location.moduleSpecifier : undefined) ||
(isVariableDeclaration(location) && location.initializer && isRequireCall(location.initializer, /*requireStringLiteralLikeArgument*/ true) ? location.initializer.arguments[0] : undefined) ||
findAncestor(location, isImportCall)?.arguments[0] ||
findAncestor(location, isImportDeclaration)?.moduleSpecifier ||
Expand Down Expand Up @@ -9800,12 +9804,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.ImportClause: {
const generatedSpecifier = getSpecifierForModuleSymbol(target.parent || target, context); // generate specifier (even though we're reusing and existing one) for ambient module reference include side effects
const specifier = bundled ? factory.createStringLiteral(generatedSpecifier) : (node as ImportClause).parent.moduleSpecifier;
const attributes = isImportDeclaration(node.parent) ? node.parent.attributes : undefined;
const isTypeOnly = isJSDocImportTag((node as ImportClause).parent);
addResult(
factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(/*isTypeOnly*/ false, factory.createIdentifier(localName), /*namedBindings*/ undefined),
factory.createImportClause(isTypeOnly, factory.createIdentifier(localName), /*namedBindings*/ undefined),
specifier,
(node as ImportClause).parent.attributes,
attributes,
),
ModifierFlags.None,
);
Expand All @@ -9814,10 +9820,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.NamespaceImport: {
const generatedSpecifier = getSpecifierForModuleSymbol(target.parent || target, context); // generate specifier (even though we're reusing and existing one) for ambient module reference include side effects
const specifier = bundled ? factory.createStringLiteral(generatedSpecifier) : (node as NamespaceImport).parent.parent.moduleSpecifier;
const isTypeOnly = isJSDocImportTag((node as NamespaceImport).parent.parent);
addResult(
factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, factory.createNamespaceImport(factory.createIdentifier(localName))),
factory.createImportClause(isTypeOnly, /*name*/ undefined, factory.createNamespaceImport(factory.createIdentifier(localName))),
specifier,
(node as ImportClause).parent.attributes,
),
Expand All @@ -9839,11 +9846,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.ImportSpecifier: {
const generatedSpecifier = getSpecifierForModuleSymbol(target.parent || target, context); // generate specifier (even though we're reusing and existing one) for ambient module reference include side effects
const specifier = bundled ? factory.createStringLiteral(generatedSpecifier) : (node as ImportSpecifier).parent.parent.parent.moduleSpecifier;
const isTypeOnly = isJSDocImportTag((node as ImportSpecifier).parent.parent.parent);
addResult(
factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(
/*isTypeOnly*/ false,
isTypeOnly,
/*name*/ undefined,
factory.createNamedImports([
factory.createImportSpecifier(
Expand Down Expand Up @@ -42193,6 +42201,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function checkJSDocImportTag(node: JSDocImportTag) {
checkImportAttributes(node);
}

function checkJSDocImplementsTag(node: JSDocImplementsTag): void {
const classLike = getEffectiveJSDocHost(node);
if (!classLike || !isClassDeclaration(classLike) && !isClassExpression(classLike)) {
Expand Down Expand Up @@ -46402,7 +46414,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function checkImportAttributes(declaration: ImportDeclaration | ExportDeclaration) {
function checkImportAttributes(declaration: ImportDeclaration | ExportDeclaration | JSDocImportTag) {
const node = declaration.attributes;
if (node) {
const importAttributesType = getGlobalImportAttributesType(/*reportErrors*/ true);
Expand All @@ -46429,7 +46441,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return grammarErrorOnNode(node, message);
}

if (isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly) {
const isTypeOnly = isJSDocImportTag(declaration) || (isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly);
if (isTypeOnly) {
return grammarErrorOnNode(node, isImportAttributes ? Diagnostics.Import_attributes_cannot_be_used_with_type_only_imports_or_exports : Diagnostics.Import_assertions_cannot_be_used_with_type_only_imports_or_exports);
}

Expand Down Expand Up @@ -46957,6 +46970,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return checkJSDocSatisfiesTag(node as JSDocSatisfiesTag);
case SyntaxKind.JSDocThisTag:
return checkJSDocThisTag(node as JSDocThisTag);
case SyntaxKind.JSDocImportTag:
return checkJSDocImportTag(node as JSDocImportTag);
case SyntaxKind.IndexedAccessType:
return checkIndexedAccessType(node as IndexedAccessTypeNode);
case SyntaxKind.MappedType:
Expand Down Expand Up @@ -47913,6 +47928,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (
(isExternalModuleImportEqualsDeclaration(node.parent.parent) && getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) ||
((node.parent.kind === SyntaxKind.ImportDeclaration || node.parent.kind === SyntaxKind.ExportDeclaration) && (node.parent as ImportDeclaration).moduleSpecifier === node) ||
(isInJSFile(node) && isJSDocImportTag(node.parent) && node.parent.moduleSpecifier === node) ||
((isInJSFile(node) && isRequireCall(node.parent, /*requireStringLiteralLikeArgument*/ false)) || isImportCall(node.parent)) ||
(isLiteralTypeNode(node.parent) && isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent)
) {
Expand Down
22 changes: 22 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ import {
JSDocEnumTag,
JSDocFunctionType,
JSDocImplementsTag,
JSDocImportTag,
JSDocNameReference,
JSDocNonNullableType,
JSDocNullableType,
Expand Down Expand Up @@ -1826,6 +1827,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return emitJSDocTypedefTag(node as JSDocTypedefTag);
case SyntaxKind.JSDocSeeTag:
return emitJSDocSeeTag(node as JSDocSeeTag);
case SyntaxKind.JSDocImportTag:
return emitJSDocImportTag(node as JSDocImportTag);
// SyntaxKind.JSDocPropertyTag (see JSDocParameterTag, above)

// Transformation nodes
Expand Down Expand Up @@ -4006,6 +4009,25 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
emitJSDocComment(tag.comment);
}

function emitJSDocImportTag(tag: JSDocImportTag) {
emitJSDocTagName(tag.tagName);
writeSpace();

if (tag.importClause) {
emit(tag.importClause);
writeSpace();

emitTokenWithComment(SyntaxKind.FromKeyword, tag.importClause.end, writeKeyword, tag);
writeSpace();
}

emitExpression(tag.moduleSpecifier);
if (tag.attributes) {
emitWithLeadingSpace(tag.attributes);
}
emitJSDocComment(tag.comment);
}

function emitJSDocNameReference(node: JSDocNameReference) {
writeSpace();
writePunctuation("{");
Expand Down
25 changes: 25 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ import {
JSDocEnumTag,
JSDocFunctionType,
JSDocImplementsTag,
JSDocImportTag,
JSDocLink,
JSDocLinkCode,
JSDocLinkPlain,
Expand Down Expand Up @@ -857,6 +858,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
updateJSDocImplementsTag,
createJSDocSeeTag,
updateJSDocSeeTag,
createJSDocImportTag,
updateJSDocImportTag,
createJSDocNameReference,
updateJSDocNameReference,
createJSDocMemberName,
Expand Down Expand Up @@ -5523,6 +5526,26 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

// @api
function createJSDocImportTag(tagName: Identifier | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression, attributes?: ImportAttributes, comment?: string | NodeArray<JSDocComment>): JSDocImportTag {
const node = createBaseJSDocTag<JSDocImportTag>(SyntaxKind.JSDocImportTag, tagName ?? createIdentifier("import"), comment);
node.importClause = importClause;
node.moduleSpecifier = moduleSpecifier;
node.attributes = attributes;
node.comment = comment;
return node;
}

function updateJSDocImportTag(node: JSDocImportTag, tagName: Identifier | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression, attributes: ImportAttributes | undefined, comment: string | NodeArray<JSDocComment> | undefined): JSDocImportTag {
return node.tagName !== tagName
|| node.comment !== comment
|| node.importClause !== importClause
|| node.moduleSpecifier !== moduleSpecifier
|| node.attributes !== attributes
? update(createJSDocImportTag(tagName, importClause, moduleSpecifier, attributes, comment), node)
: node;
}

// @api
function createJSDocText(text: string): JSDocText {
const node = createBaseNode<JSDocText>(SyntaxKind.JSDocText);
Expand Down Expand Up @@ -7179,6 +7202,8 @@ function getDefaultTagNameForKind(kind: JSDocTag["kind"]): string {
return "augments";
case SyntaxKind.JSDocImplementsTag:
return "implements";
case SyntaxKind.JSDocImportTag:
return "import";
default:
return Debug.fail(`Unsupported kind: ${Debug.formatSyntaxKind(kind)}`);
}
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 @@ -91,6 +91,7 @@ import {
JSDocEnumTag,
JSDocFunctionType,
JSDocImplementsTag,
JSDocImportTag,
JSDocLink,
JSDocLinkCode,
JSDocLinkPlain,
Expand Down Expand Up @@ -1183,6 +1184,10 @@ export function isJSDocThrowsTag(node: Node): node is JSDocThrowsTag {
return node.kind === SyntaxKind.JSDocThrowsTag;
}

export function isJSDocImportTag(node: Node): node is JSDocImportTag {
return node.kind === SyntaxKind.JSDocImportTag;
}

// Synthesized list

/** @internal */
Expand Down
Loading

0 comments on commit 2de69b0

Please sign in to comment.