diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..ad150911468f2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -51218,6 +51218,116 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return nodeBuilder.serializeTypeForExpression(expr, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, internalFlags, tracker); } + function createTypeOfTypeNode(typeNodeIn: TypeNode, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker) { + const typeNode = getParseTreeNode(typeNodeIn, isTypeNode) || typeNodeIn; + if (!typeNode) { + return factory.createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; + } + return nodeBuilder.typeToTypeNode(getTypeFromTypeNode(typeNode), enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, internalFlags, tracker); + } + + function createTypeLiteralOfTypeNode(typeNodeIn: TypeNode, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker) { + const typeNode = getParseTreeNode(typeNodeIn, isTypeNode) || typeNodeIn; + if (!typeNode) return; + return createTypeLiteralOfType(getTypeFromTypeNode(typeNode), enclosingDeclaration, flags, internalFlags, tracker); + } + + function createTypeLiteralOfClassDeclaration(declarationIn: ClassDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker) { + const declaration = getParseTreeNode(declarationIn, isClassDeclaration) || declarationIn; + if (!declaration) return; + const schemaType = getTypeOfClassSchemaProperty(declaration, "Type"); + if (schemaType) { + return createTypeLiteralOfType(schemaType, enclosingDeclaration, flags, internalFlags, tracker); + } + const symbol = getSymbolOfDeclaration(declaration); + if (!symbol) return; + return createTypeLiteralOfType(getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, flags, internalFlags, tracker); + } + + function createMakeTypeOfClassDeclaration(declarationIn: ClassDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker) { + const declaration = getParseTreeNode(declarationIn, isClassDeclaration) || declarationIn; + if (!declaration) return; + const makeType = getTypeOfClassSchemaProperty(declaration, "~type.make.in"); + const typeType = getTypeOfClassSchemaProperty(declaration, "Type"); + if (!makeType || !typeType) return; + return createMakeTypeOfTypes(makeType, typeType, enclosingDeclaration, flags, internalFlags, tracker); + } + + function createTypeOfClassStaticProperty(declarationIn: ClassDeclaration, propertyName: string, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker) { + const declaration = getParseTreeNode(declarationIn, isClassDeclaration) || declarationIn; + if (!declaration) return; + const propertyType = getTypeOfClassSchemaProperty(declaration, propertyName) || getTypeOfClassStaticProperty(declaration, propertyName); + if (!propertyType) return; + return nodeBuilder.typeToTypeNode(propertyType, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals | NodeBuilderFlags.UseFullyQualifiedType, internalFlags, tracker); + } + + function getTypeOfClassSchemaProperty(declaration: ClassDeclaration, propertyName: string) { + const schemaExpression = getClassSchemaExpression(declaration); + if (!schemaExpression) return; + const schemaType = getTypeOfExpression(schemaExpression); + const property = getPropertyOfType(schemaType, escapeLeadingUnderscores(propertyName)); + return property && getTypeOfSymbolAtLocation(property, schemaExpression); + } + + function getClassSchemaExpression(declaration: ClassDeclaration): Expression | undefined { + const heritageClause = declaration.heritageClauses?.[0]; + const heritageType = heritageClause?.types[0]; + const expression = heritageType?.expression; + return expression && isCallExpression(expression) && expression.arguments.length > 0 ? expression.arguments[0] : undefined; + } + + function getTypeOfClassStaticProperty(declaration: ClassDeclaration, propertyName: string) { + const symbol = getSymbolOfDeclaration(declaration); + if (!symbol) return; + const staticType = getTypeOfSymbol(symbol); + const property = getPropertyOfType(staticType, escapeLeadingUnderscores(propertyName)); + return property && getTypeOfSymbolAtLocation(property, declaration); + } + + function createTypeLiteralOfType(type: Type, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker) { + const members = map(getPropertiesOfType(type), property => { + const name = unescapeLeadingUnderscores(property.escapedName); + const propertyType = getTypeOfSymbol(property); + const propertyTypeNode = nodeBuilder.typeToTypeNode(propertyType, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals | NodeBuilderFlags.UseFullyQualifiedType, internalFlags, tracker) + || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); + return factory.createPropertySignature( + [factory.createModifier(SyntaxKind.ReadonlyKeyword)], + isIdentifierText(name, ScriptTarget.ESNext) ? factory.createIdentifier(name) : factory.createStringLiteral(name), + property.flags & SymbolFlags.Optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined, + propertyTypeNode, + ); + }); + return factory.createTypeLiteralNode(members); + } + + function createMakeTypeOfTypes(makeType: Type, typeType: Type, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker) { + const isVoidish = (type: Type) => !!(type.flags & (TypeFlags.Void | TypeFlags.Undefined)); + const makeTypes = makeType.flags & TypeFlags.Union ? (makeType as UnionType).types : undefined; + const hasVoid = !!makeTypes?.some(isVoidish); + const objectMakeType = makeTypes + ? makeTypes.find(type => getPropertiesOfType(type).length > 0) || makeType + : makeType; + const makeProperties = getPropertiesOfType(objectMakeType); + if (!makeProperties.length) { + return nodeBuilder.typeToTypeNode(makeType, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals | NodeBuilderFlags.UseFullyQualifiedType, internalFlags, tracker); + } + const typeProperties = new Map(getPropertiesOfType(typeType).map(property => [property.escapedName, property])); + const literal = factory.createTypeLiteralNode(map(makeProperties, property => { + const name = unescapeLeadingUnderscores(property.escapedName); + const source = typeProperties.get(property.escapedName) || property; + const propertyType = getTypeOfSymbol(source); + const propertyTypeNode = nodeBuilder.typeToTypeNode(propertyType, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals | NodeBuilderFlags.UseFullyQualifiedType, internalFlags, tracker) + || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); + return factory.createPropertySignature( + [factory.createModifier(SyntaxKind.ReadonlyKeyword)], + isIdentifierText(name, ScriptTarget.ESNext) ? factory.createIdentifier(name) : factory.createStringLiteral(name), + property.flags & SymbolFlags.Optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined, + propertyTypeNode, + ); + })); + return hasVoid ? factory.createUnionTypeNode([literal, factory.createKeywordTypeNode(SyntaxKind.VoidKeyword)]) : literal; + } + function hasGlobalName(name: string): boolean { return globals.has(escapeLeadingUnderscores(name)); } @@ -51408,6 +51518,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { createTypeOfDeclaration, createReturnTypeOfSignatureDeclaration, createTypeOfExpression, + createTypeOfTypeNode, + createTypeLiteralOfTypeNode, + createTypeLiteralOfClassDeclaration, + createMakeTypeOfClassDeclaration, + createTypeOfClassStaticProperty, createLiteralConstValue, isSymbolAccessible, isEntityNameVisible, diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 5a4858b572dc1..52d211a80e583 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1162,10 +1162,15 @@ export const notImplementedResolver: EmitResolver = { requiresAddingImplicitUndefined: notImplemented, isExpandoFunctionDeclaration: notImplemented, getPropertiesOfContainerFunction: notImplemented, - createTypeOfDeclaration: notImplemented, - createReturnTypeOfSignatureDeclaration: notImplemented, - createTypeOfExpression: notImplemented, - createLiteralConstValue: notImplemented, + createTypeOfDeclaration: notImplemented, + createReturnTypeOfSignatureDeclaration: notImplemented, + createTypeOfExpression: notImplemented, + createTypeOfTypeNode: notImplemented, + createTypeLiteralOfTypeNode: notImplemented, + createTypeLiteralOfClassDeclaration: notImplemented, + createMakeTypeOfClassDeclaration: notImplemented, + createTypeOfClassStaticProperty: notImplemented, + createLiteralConstValue: notImplemented, isSymbolAccessible: notImplemented, isEntityNameVisible: notImplemented, // Returns the constant value this property access resolves to: notImplemented, or 'undefined' for a non-constant diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index b1340ccfd7f92..bffb7b29583ba 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -37,6 +37,7 @@ import { EmitResolver, emptyArray, EntityNameOrEntityNameExpression, + EntityName, EnumDeclaration, ExportAssignment, ExportDeclaration, @@ -81,6 +82,7 @@ import { hasSyntacticModifier, HeritageClause, Identifier, + idText, ImportAttributes, ImportDeclaration, ImportEqualsDeclaration, @@ -88,6 +90,7 @@ import { IndexSignatureDeclaration, InterfaceDeclaration, InternalNodeBuilderFlags, + IntersectionTypeNode, isAmbientModule, isArray, isArrayBindingElement, @@ -128,6 +131,7 @@ import { isObjectLiteralExpression, isOmittedExpression, isParameter, + isPropertyAccessExpression, isPrimitiveLiteralValue, isPrivateIdentifier, isSemicolonClassElement, @@ -202,6 +206,7 @@ import { transformNodes, tryCast, TypeAliasDeclaration, + TypeElement, TypeNode, TypeParameterDeclaration, TypeReferenceNode, @@ -533,6 +538,7 @@ export function transformDeclarations(context: TransformationContext): Transform else { const statements = visitNodes(node.statements, visitDeclarationStatements, isStatement); combinedStatements = setTextRange(factory.createNodeArray(transformAndReplaceLatePaintedStatements(statements)), node.statements); + combinedStatements = setTextRange(factory.createNodeArray(createEffectSchemaSourceFileDeclarations(combinedStatements)), combinedStatements); if (isExternalModule(node) && (!resultHasExternalModuleIndicator || (needsScopeFixMarker && !resultHasScopeMarker))) { combinedStatements = setTextRange(factory.createNodeArray([...combinedStatements, createEmptyExports(factory)]), combinedStatements); } @@ -1561,18 +1567,23 @@ export function transformDeclarations(context: TransformationContext): Transform lateStatements = visitNodes(lateStatements, stripExportModifiers, isStatement); } } + const schemaModelDeclarations = createEffectSchemaModelDeclarations(input, lateStatements); + if (schemaModelDeclarations) { + lateStatements = factory.createNodeArray(schemaModelDeclarations.namespaceStatements); + } const body = factory.updateModuleBlock(inner, lateStatements); needsDeclare = previousNeedsDeclare; needsScopeFixMarker = oldNeedsScopeFix; resultHasScopeMarker = oldHasScopeFix; const mods = ensureModifiers(input); - return cleanup(updateModuleDeclarationAndKeyword( + const updated = cleanup(updateModuleDeclarationAndKeyword( input, mods, isExternalModuleAugmentation(input) ? rewriteModuleSpecifier(input, input.name) : input.name, body, )); + return schemaModelDeclarations?.typeInterface && updated ? [schemaModelDeclarations.typeInterface, updated] : updated; } else { needsDeclare = previousNeedsDeclare; @@ -1860,6 +1871,331 @@ export function transformDeclarations(context: TransformationContext): Transform clause => clause.types && !!clause.types.length, )); } + + function createEffectSchemaModelDeclarations(input: ModuleDeclaration, statements: NodeArray) { + if (input.name.kind !== SyntaxKind.Identifier) return; + const encoded = statements.find(isEffectSchemaStructNestedEncodedInterface); + if (!encoded) return; + + const encodedLiteral = materializeSchemaNestedEncoded(encoded); + + let changed = false; + const namespaceStatements: Statement[] = [...flatMap(statements, statement => { + if (statement === encoded && encodedLiteral) { + changed = true; + const next: Statement[] = [factory.createInterfaceDeclaration( + encoded.modifiers, + encoded.name, + encoded.typeParameters, + /*heritageClauses*/ undefined, + encodedLiteral.members, + )]; + return next; + } + return [statement]; + })]; + + return changed ? { typeInterface: undefined, namespaceStatements } : undefined; + } + + function isEffectSchemaStructNestedEncodedInterface(statement: Statement): statement is InterfaceDeclaration { + if (!isInterfaceDeclaration(statement) || idText(statement.name) !== "Encoded") return false; + const clause = statement.heritageClauses?.[0]; + const heritageType = clause?.types[0]; + if (!heritageType || clause!.types.length !== 1 || heritageType.typeArguments?.length !== 1) return false; + const expression = heritageType.expression; + return isPropertyAccessExpression(expression) + && idText(expression.name) === "StructNestedEncoded" + && heritageType.typeArguments[0].kind === SyntaxKind.TypeQuery; + } + + function materializeSchemaNestedEncoded(encoded: InterfaceDeclaration) { + const heritageType = encoded.heritageClauses![0].types[0]; + const typeName = expressionToEntityName(heritageType.expression); + if (!typeName) return; + const typeNode = factory.createTypeReferenceNode(typeName, heritageType.typeArguments); + setParent(typeNode, encoded); + return resolver.createTypeLiteralOfTypeNode(typeNode, enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker); + } + + function createEffectSchemaSourceFileDeclarations(statements: NodeArray): Statement[] { + const classes = new Map(); + const sourceNamespaces = new Set(); + for (const statement of statements) { + if (isEffectSchemaModelNamespace(statement)) { + sourceNamespaces.add(idText(statement.name)); + } + const className = getEffectSchemaClassName(statement); + if (className && isClassDeclaration(statement)) { + classes.set(className, statement); + } + } + // A model is a class that has BOTH a source `namespace X { interface Encoded }` and a + // class declaration. (Bare `S.Opaque` models without a source namespace are left as-is + // for now — see docs/planning/dts-emit-schema-codegen.md §5f.) + const modelNames = new Set(); + for (const name of sourceNamespaces) { + if (classes.has(name)) modelNames.add(name); + } + if (!modelNames.size) return [...statements]; + + let changed = false; + const next: Statement[] = []; + for (const statement of statements) { + const baseModelName = getEffectSchemaBaseModelName(statement); + if (baseModelName && modelNames.has(baseModelName)) { + const classDeclaration = classes.get(baseModelName); + const updated = classDeclaration && updateEffectSchemaBaseDeclaration(statement, baseModelName, classDeclaration); + if (updated) { + changed = true; + next.push(updated); + continue; + } + } + if (isEffectSchemaModelNamespace(statement) && modelNames.has(idText(statement.name))) { + const classDeclaration = classes.get(idText(statement.name)); + const updated = classDeclaration && updateEffectSchemaNamespaceDeclaration(statement, classDeclaration); + if (updated) { + changed = true; + next.push(updated); + continue; + } + } + const className = getEffectSchemaClassName(statement); + if (className && modelNames.has(className)) { + if (!hasTopLevelInterface(statements, className)) { + const typeInterface = createEffectSchemaTypeInterface(statement as ClassDeclaration); + if (typeInterface) { + changed = true; + next.push(statement, typeInterface); + continue; + } + } + } + next.push(statement); + } + return changed ? next : [...statements]; + } + + function isEffectSchemaModelNamespace(statement: Statement): statement is ModuleDeclaration & { name: Identifier } { + if (!isModuleDeclaration(statement) || statement.name.kind !== SyntaxKind.Identifier) return false; + const body = statement.body; + // Detect a model namespace by its `Encoded` interface. Note: path-1 (namespace-level) + // has already expanded `interface Encoded extends StructNestedEncoded<...>` into a + // literal `interface Encoded {...}` by the time this source-file pass runs, so we match + // any `interface Encoded` member (not the now-stripped heritage). Edge cases that emit + // `type Encoded = ...` (alias) are excluded here and keep their original Opaque base. + return !!body && body.kind === SyntaxKind.ModuleBlock && some(body.statements, member => + isInterfaceDeclaration(member) && idText(member.name) === "Encoded" + ); + } + + function getEffectSchemaClassName(statement: Statement): string | undefined { + return isClassDeclaration(statement) && statement.name ? idText(statement.name) : undefined; + } + + function hasTopLevelInterface(statements: NodeArray, name: string): boolean { + return some(statements, statement => isInterfaceDeclaration(statement) && idText(statement.name) === name); + } + + function createEffectSchemaTypeInterface(classDeclaration: ClassDeclaration) { + if (!classDeclaration.name) return; + const literal = resolver.createTypeLiteralOfClassDeclaration(classDeclaration, enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker); + return literal && factory.createInterfaceDeclaration( + [factory.createModifier(SyntaxKind.ExportKeyword)], + classDeclaration.name, + /*typeParameters*/ undefined, + /*heritageClauses*/ undefined, + literal.members, + ); + } + + function updateEffectSchemaNamespaceDeclaration(namespace: ModuleDeclaration & { name: Identifier }, classDeclaration: ClassDeclaration) { + const body = namespace.body; + if (!body || body.kind !== SyntaxKind.ModuleBlock) return; + const existing = new Set(); + const kept: Statement[] = []; + for (const statement of body.statements) { + if ((isInterfaceDeclaration(statement) || isTypeAliasDeclaration(statement)) && idText(statement.name) !== "Encoded") { + existing.add(idText(statement.name)); + if (idText(statement.name) === "Make" || idText(statement.name) === "DecodingServices" || idText(statement.name) === "EncodingServices") { + continue; + } + } + kept.push(statement); + } + const makeDeclaration = createEffectSchemaMakeDeclaration(classDeclaration); + const decodingServices = createEffectSchemaServiceDeclaration(classDeclaration, "DecodingServices"); + const encodingServices = createEffectSchemaServiceDeclaration(classDeclaration, "EncodingServices"); + const additions: Statement[] = []; + if (makeDeclaration) additions.push(makeDeclaration); + if (decodingServices) additions.push(decodingServices); + if (encodingServices) additions.push(encodingServices); + if (!additions.length && existing.size === 0) return; + return factory.updateModuleDeclaration( + namespace, + namespace.modifiers, + namespace.name, + factory.updateModuleBlock(body, factory.createNodeArray([...kept, ...additions])), + ); + } + + function createEffectSchemaMakeDeclaration(classDeclaration: ClassDeclaration) { + const makeType = resolver.createMakeTypeOfClassDeclaration(classDeclaration, enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker) + || resolver.createTypeOfClassStaticProperty(classDeclaration, "~type.make.in", enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker); + if (!makeType) return; + if (isTypeLiteralNode(makeType)) { + return factory.createInterfaceDeclaration( + /*modifiers*/ undefined, + factory.createIdentifier("Make"), + /*typeParameters*/ undefined, + /*heritageClauses*/ undefined, + makeType.members, + ); + } + return factory.createTypeAliasDeclaration( + /*modifiers*/ undefined, + factory.createIdentifier("Make"), + /*typeParameters*/ undefined, + makeType, + ); + } + + function createEffectSchemaServiceDeclaration(classDeclaration: ClassDeclaration, name: "DecodingServices" | "EncodingServices") { + const resolved = resolver.createTypeOfClassStaticProperty(classDeclaration, name, enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker); + const serviceType = resolved?.kind === SyntaxKind.AnyKeyword ? factory.createKeywordTypeNode(SyntaxKind.NeverKeyword) : resolved; + return serviceType && factory.createTypeAliasDeclaration( + /*modifiers*/ undefined, + factory.createIdentifier(name), + /*typeParameters*/ undefined, + serviceType, + ); + } + + function isEffectSchemaOpaqueCtor(name: string): boolean { + switch (name) { + case "Opaque": + case "OpaqueClass": + case "OpaqueFacade": + case "Class": + case "TaggedClass": + return true; + default: + return false; + } + } + + function getEffectSchemaBaseModelName(statement: Statement): string | undefined { + if (statement.kind !== SyntaxKind.VariableStatement) return; + const variableStatement = statement as VariableStatement; + if (variableStatement.declarationList.declarations.length !== 1) return; + const declaration = variableStatement.declarationList.declarations[0]; + // The synthesized `X_base` const uses a generated identifier whose text isn't + // reliably readable, so detect the model from the base type's first type argument: + // `S.Opaque` / `S.OpaqueFacade & {...}` -> `Model`. + let typeNode = declaration.type; + if (typeNode && typeNode.kind === SyntaxKind.IntersectionType) { + typeNode = (typeNode as IntersectionTypeNode).types[0]; + } + if (!typeNode || typeNode.kind !== SyntaxKind.TypeReference) return; + const typeRef = typeNode as TypeReferenceNode; + // Only schema model bases: `S.< Model, ... >`. Gating on the constructor + // namespace (`S`) + name excludes unrelated complex-heritage classes (Context.Service, + // Data.TaggedError, MiddlewareMaker.*, ...) that also get a synthesized `_base` const. + const ctor = typeRef.typeName; + if ( + ctor.kind !== SyntaxKind.QualifiedName + || ctor.left.kind !== SyntaxKind.Identifier + || idText(ctor.left as Identifier) !== "S" + || !isEffectSchemaOpaqueCtor(idText(ctor.right)) + ) { + return; + } + const firstArg = typeRef.typeArguments?.[0]; + if (!firstArg || firstArg.kind !== SyntaxKind.TypeReference) return; + const firstName = (firstArg as TypeReferenceNode).typeName; + // First type arg of `S.Opaque` is the model. + // (We only rewrite bases whose model also has a class declaration — gated by the caller.) + return firstName.kind === SyntaxKind.Identifier ? idText(firstName) : undefined; + } + + function updateEffectSchemaBaseDeclaration(statement: Statement, modelName: string, classDeclaration: ClassDeclaration): VariableStatement | undefined { + if (statement.kind !== SyntaxKind.VariableStatement) return; + const variableStatement = statement as VariableStatement; + const declaration = variableStatement.declarationList.declarations[0]; + if (!declaration || declaration.name.kind !== SyntaxKind.Identifier) return; + const updatedDeclaration = factory.updateVariableDeclaration( + declaration, + declaration.name, + declaration.exclamationToken, + createEffectSchemaFacadeBaseType(modelName, classDeclaration), + declaration.initializer, + ); + return factory.updateVariableStatement( + variableStatement, + variableStatement.modifiers, + factory.updateVariableDeclarationList(variableStatement.declarationList, [updatedDeclaration]), + ); + } + + function createEffectSchemaFacadeBaseType(modelName: string, classDeclaration: ClassDeclaration): TypeNode { + const model = factory.createIdentifier(modelName); + const schemaStaticMembers = createEffectSchemaStaticMembers(classDeclaration); + // Reference the namespace view members so the facade reads + // `S.OpaqueFacade`. + const view = (name: string) => factory.createTypeReferenceNode(factory.createQualifiedName(model, factory.createIdentifier(name))); + return factory.createIntersectionTypeNode([ + factory.createTypeReferenceNode(factory.createQualifiedName(factory.createIdentifier("S"), factory.createIdentifier("OpaqueFacade")), [ + factory.createTypeReferenceNode(model), + view("Encoded"), + view("Make"), + view("DecodingServices"), + view("EncodingServices"), + factory.createTypeLiteralNode([]), + ]), + factory.createTypeLiteralNode(schemaStaticMembers), + ]); + } + + function createEffectSchemaStaticMembers(classDeclaration: ClassDeclaration): TypeElement[] { + const members: TypeElement[] = []; + addSchemaStaticMember(members, classDeclaration, "fields", /*readonly*/ true); + addSchemaStaticMember(members, classDeclaration, "mapFields", /*readonly*/ false); + addSchemaStaticMember(members, classDeclaration, "to", /*readonly*/ true); + addSchemaStaticMember(members, classDeclaration, "from", /*readonly*/ true); + addSchemaStaticMember(members, classDeclaration, "copy", /*readonly*/ true); + return members; + } + + function addSchemaStaticMember(members: TypeElement[], classDeclaration: ClassDeclaration, name: string, isReadonly: boolean) { + const type = resolver.createTypeOfClassStaticProperty(classDeclaration, name, enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker); + if (!type || type.kind === SyntaxKind.AnyKeyword) return; + members.push(factory.createPropertySignature( + isReadonly ? [factory.createModifier(SyntaxKind.ReadonlyKeyword)] : undefined, + name, + /*questionToken*/ undefined, + type, + )); + } + + function getEffectSchemaServiceType(classDeclaration: ClassDeclaration, name: "DecodingServices" | "EncodingServices") { + const resolved = resolver.createTypeOfClassStaticProperty(classDeclaration, name, enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, symbolTracker); + return resolved?.kind === SyntaxKind.AnyKeyword + ? factory.createKeywordTypeNode(SyntaxKind.NeverKeyword) + : resolved || factory.createKeywordTypeNode(SyntaxKind.NeverKeyword); + } + + function expressionToEntityName(expression: Expression): EntityName | undefined { + if (expression.kind === SyntaxKind.Identifier) { + return expression as Identifier; + } + if (isPropertyAccessExpression(expression)) { + const left = expressionToEntityName(expression.expression); + if (!left || expression.name.kind !== SyntaxKind.Identifier) return; + return left && factory.createQualifiedName(left, expression.name); + } + return undefined; + } } function isAlwaysType(node: Node) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4d8d22afb54e6..fed25d27cac97 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5912,10 +5912,15 @@ export interface EmitResolver { requiresAddingImplicitUndefined(node: ParameterDeclaration, enclosingDeclaration: Node | undefined): boolean; isExpandoFunctionDeclaration(node: FunctionDeclaration | VariableDeclaration): boolean; getPropertiesOfContainerFunction(node: Declaration): Symbol[]; - createTypeOfDeclaration(declaration: HasInferredType, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; - createReturnTypeOfSignatureDeclaration(signatureDeclaration: SignatureDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; - createTypeOfExpression(expr: Expression, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; - createLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, tracker: SymbolTracker): Expression; + createTypeOfDeclaration(declaration: HasInferredType, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; + createReturnTypeOfSignatureDeclaration(signatureDeclaration: SignatureDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; + createTypeOfExpression(expr: Expression, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; + createTypeOfTypeNode(typeNode: TypeNode, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; + createTypeLiteralOfTypeNode(typeNode: TypeNode, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeLiteralNode | undefined; + createTypeLiteralOfClassDeclaration(declaration: ClassDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeLiteralNode | undefined; + createMakeTypeOfClassDeclaration(declaration: ClassDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; + createTypeOfClassStaticProperty(declaration: ClassDeclaration, propertyName: string, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined; + createLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, tracker: SymbolTracker): Expression; isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags | undefined, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult; isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult; // Returns the constant value this property access resolves to, or 'undefined' for a non-constant