import ts from "typescript"; export type StaticModuleReferenceKind = | "amd" | "lib" | "module" | "path" | "types"; export type ModuleReference = | Readonly<{ kind: "nonliteral"; diagnostic: string }> | Readonly<{ kind: "specifier"; referenceKind: StaticModuleReferenceKind; specifier: string; }>; function literalReference( expression: ts.Expression | undefined, diagnostic: string, referenceKind: StaticModuleReferenceKind = "module", ): ModuleReference { return expression !== undefined && ts.isStringLiteralLike(expression) ? { kind: "specifier", referenceKind, specifier: expression.text } : { diagnostic, kind: "nonliteral" }; } function staticReference( referenceKind: StaticModuleReferenceKind, specifier: string, ): ModuleReference { return { kind: "specifier", referenceKind, specifier }; } function accessName(expression: ts.Expression): string | undefined { if (ts.isPropertyAccessExpression(expression)) { return expression.name.text; } if ( ts.isElementAccessExpression(expression) && expression.argumentExpression !== undefined && ts.isStringLiteralLike(expression.argumentExpression) ) { return expression.argumentExpression.text; } return undefined; } function accessReceiver(expression: ts.Expression): ts.Expression | undefined { return ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression) ? expression.expression : undefined; } function isIdentifier(expression: ts.Expression, name: string): boolean { return ts.isIdentifier(expression) && expression.text === name; } function isRequireLikeExpression(expression: ts.Expression): boolean { if (isIdentifier(expression, "require")) { return true; } const receiver = accessReceiver(expression); return accessName(expression) === "require" || (receiver !== undefined && isRequireLikeExpression(receiver)); } function isOptionalAccess(expression: ts.Expression): boolean { return ( (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) && expression.questionDotToken !== undefined ); } type RequireCallKind = "direct" | "module" | "resolve"; function getRequireCallKind( expression: ts.Expression, ): RequireCallKind | undefined { if (isIdentifier(expression, "require")) return "direct"; if (!ts.isPropertyAccessExpression(expression) || isOptionalAccess(expression)) { return undefined; } const receiver = expression.expression; if (accessName(expression) === "require" && isIdentifier(receiver, "module")) { return "module"; } return accessName(expression) === "resolve" && isIdentifier(receiver, "require") ? "resolve" : undefined; } function isWithinCallCallee(node: ts.Node): boolean { let current = node; while ( ts.isPropertyAccessExpression(current.parent) || ts.isElementAccessExpression(current.parent) ) { current = current.parent; } return ts.isCallExpression(current.parent) && current.parent.expression === current; } function isAccessName(node: ts.Node): boolean { return ( (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) || (ts.isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) ); } export function getModuleReferences( filePath: string, source: string, ): readonly ModuleReference[] { const sourceFile = ts.createSourceFile( filePath, source, ts.ScriptTarget.Latest, true, ); const references: ModuleReference[] = []; let hasJsxDiagnostic = false; references.push( ...sourceFile.referencedFiles.map(({ fileName }) => staticReference("path", fileName), ), ...sourceFile.typeReferenceDirectives.map(({ fileName }) => staticReference("types", fileName), ), ...sourceFile.libReferenceDirectives.map(({ fileName }) => staticReference("lib", fileName), ), ...sourceFile.amdDependencies.map(({ path }) => staticReference("amd", path), ), ); function visit(node: ts.Node): void { if ( !hasJsxDiagnostic && !/\/modules\/built-ins\/[^/]+\/react\//.test(filePath.replaceAll("\\", "/")) && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) ) { references.push({ diagnostic: "JSX syntax is forbidden in capability production modules", kind: "nonliteral", }); hasJsxDiagnostic = true; } if (ts.isImportDeclaration(node)) { references.push( literalReference( node.moduleSpecifier, "nonliteral import declaration", ), ); } else if (ts.isExportDeclaration(node)) { if (node.moduleSpecifier !== undefined) { references.push( literalReference( node.moduleSpecifier, "nonliteral export declaration", ), ); } } else if ( ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) ) { references.push( literalReference( node.moduleReference.expression, "nonliteral import equals", ), ); } else if (ts.isImportTypeNode(node)) { references.push( ts.isLiteralTypeNode(node.argument) ? literalReference( node.argument.literal, "nonliteral import type", ) : { diagnostic: "nonliteral import type", kind: "nonliteral" }, ); } else if (ts.isCallExpression(node)) { if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { const hasSupportedArity = node.arguments.length === 1 || node.arguments.length === 2; references.push( literalReference( hasSupportedArity ? node.arguments[0] : undefined, "nonliteral import()", ), ); } else { const requireCallKind = getRequireCallKind(node.expression); if ( requireCallKind !== undefined && node.questionDotToken === undefined ) { const diagnostic = requireCallKind === "module" ? "nonliteral module.require()" : requireCallKind === "resolve" ? "nonliteral require.resolve()" : "nonliteral require()"; references.push( literalReference( node.arguments.length === 1 ? node.arguments[0] : undefined, diagnostic, ), ); } else if (isRequireLikeExpression(node.expression)) { references.push({ diagnostic: "unrecognized require-like call", kind: "nonliteral", }); } } } else if ( ts.isExpression(node) && isRequireLikeExpression(node) && !isAccessName(node) && !isWithinCallCallee(node) ) { const parentAccess = ts.isPropertyAccessExpression(node.parent) || ts.isElementAccessExpression(node.parent); if (!parentAccess || node.parent.expression !== node) { references.push( { diagnostic: "unrecognized require-like reference", kind: "nonliteral" }, ); } } ts.forEachChild(node, visit); } visit(sourceFile); return references; } export function getModuleSpecifiers( filePath: string, source: string, ): readonly string[] { return getModuleReferences(filePath, source).flatMap((reference) => reference.kind === "specifier" ? [reference.specifier] : [], ); }