private _visitClass()

in src/assembler.ts [1038:1344]


  private _visitClass(type: ts.Type, ctx: EmitContext): spec.ClassType | undefined {
    if (LOG.isTraceEnabled()) {
      LOG.trace(`Processing class: ${chalk.gray(ctx.namespace.join('.'))}.${chalk.cyan(type.symbol.name)}`);
    }

    if (_hasInternalJsDocTag(type.symbol)) {
      return undefined;
    }

    this._warnAboutReservedWords(type.symbol);

    const fqn = `${[this.projectInfo.name, ...ctx.namespace].join('.')}.${type.symbol.name}`;

    if (Case.pascal(type.symbol.name) !== type.symbol.name) {
      this._diagnostics.push(
        JsiiDiagnostic.JSII_8000_PASCAL_CASED_TYPE_NAMES.create(
          (type.symbol.valueDeclaration as ts.ClassDeclaration).name ??
            type.symbol.valueDeclaration ??
            type.symbol.declarations?.[0],
          type.symbol.name,
        ),
      );
    }

    const classDeclaration = type.symbol.valueDeclaration as ts.ClassDeclaration;
    for (const typeParam of classDeclaration.typeParameters ?? []) {
      this._diagnostics.push(JsiiDiagnostic.JSII_1006_GENERIC_TYPE.create(typeParam));
    }

    const jsiiType: spec.ClassType = bindings.setClassRelatedNode(
      {
        assembly: this.projectInfo.name,
        fqn,
        kind: spec.TypeKind.Class,
        name: type.symbol.name,
        namespace: ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined,
        docs: this._visitDocumentation(type.symbol, ctx).docs,
      },
      classDeclaration,
    );

    if (_isAbstract(type.symbol, jsiiType)) {
      jsiiType.abstract = true;
    }

    const erasedBases = new Array<ts.BaseType>();
    for (let base of type.getBaseTypes() ?? []) {
      if (jsiiType.base) {
        // Ignoring this - there has already been a compilation error generated by tsc here.
        continue;
      }

      //
      // base classes ("extends foo")

      // Crawl up the inheritance tree if the current base type is not exported, so we identify the type(s) to be
      // erased, and identify the closest exported base class, should there be one.
      while (base && this._isPrivateOrInternal(base.symbol)) {
        LOG.debug(
          `Base class of ${chalk.green(jsiiType.fqn)} named ${chalk.green(
            base.symbol.name,
          )} is not exported, erasing it...`,
        );
        erasedBases.push(base);
        base = (base.getBaseTypes() ?? [])[0];
      }
      if (!base || isInternalSymbol(base.symbol)) {
        // There is no exported base class to be found, pretend this class has no base class.
        continue;
      }

      // eslint-disable-next-line no-await-in-loop
      const ref = this._typeReference(
        base,
        type.symbol.valueDeclaration ?? type.symbol.declarations?.[0],
        'base class',
      );

      if (!spec.isNamedTypeReference(ref)) {
        this._diagnostics.push(
          JsiiDiagnostic.JSII_3006_TYPE_USED_AS_CLASS.create(
            base.symbol.valueDeclaration ?? base.symbol.declarations?.[0],
            ref,
          ),
        );
        continue;
      }
      this._deferUntilTypesAvailable(fqn, [ref], base.symbol.valueDeclaration, (deref) => {
        if (!spec.isClassType(deref)) {
          this._diagnostics.push(
            JsiiDiagnostic.JSII_3006_TYPE_USED_AS_CLASS.create(
              base.symbol.valueDeclaration ?? base.symbol.declarations?.[0],
              ref,
            ),
          );
        }
      });
      jsiiType.base = ref.fqn;
    }

    //
    // base interfaces ("implements foo")

    // collect all "implements" declarations from the current type and all
    // erased base types (because otherwise we lose them, see jsii#487)
    const implementsClauses = new Array<ts.HeritageClause>();
    for (const heritage of [type, ...erasedBases].map(
      (t) => (t.symbol.valueDeclaration as ts.ClassDeclaration).heritageClauses ?? [],
    )) {
      for (const clause of heritage) {
        if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
          // Handled by `getBaseTypes`
          continue;
        } else if (clause.token !== ts.SyntaxKind.ImplementsKeyword) {
          this._diagnostics.push(
            JsiiDiagnostic.JSII_9998_UNSUPPORTED_NODE.create(
              clause,
              `Ignoring ${ts.SyntaxKind[clause.token]} heritage clause`,
            ),
          );
          continue;
        }

        implementsClauses.push(clause);
      }
    }

    // process all "implements" clauses
    const allInterfaces = new Set<string>();
    const baseInterfaces = implementsClauses.map((clause) =>
      this._processBaseInterfaces(
        fqn,
        clause.types.map((t) => this._getTypeFromTypeNode(t)),
      ),
    );
    for (const { interfaces } of baseInterfaces) {
      for (const ifc of interfaces ?? []) {
        allInterfaces.add(ifc.fqn);
      }
      if (interfaces) {
        this._deferUntilTypesAvailable(jsiiType.fqn, interfaces, type.symbol.valueDeclaration, (...ifaces) => {
          for (const iface of ifaces) {
            if (spec.isInterfaceType(iface) && iface.datatype) {
              this._diagnostics.push(
                JsiiDiagnostic.JSII_3007_ILLEGAL_STRUCT_EXTENSION.create(
                  type.symbol.valueDeclaration ?? type.symbol.declarations?.[0],
                  jsiiType,
                  iface,
                ),
              );
            }
          }
        });
      }
    }

    if (allInterfaces.size > 0) {
      jsiiType.interfaces = Array.from(allInterfaces);
    }

    if (!type.isClass()) {
      throw new Error('Oh no');
    }

    const allDeclarations: Array<{
      decl: ts.Declaration;
      type: ts.InterfaceType | ts.BaseType;
    }> = (type.symbol.declarations ?? []).map((decl) => ({ decl, type }));

    // Considering erased bases' declarations, too, so they are "blended in"
    for (const base of erasedBases) {
      allDeclarations.push(
        ...(base.symbol.declarations ?? []).map((decl) => ({
          decl,
          type: base,
        })),
      );
    }

    for (const { decl, type: declaringType } of allDeclarations) {
      const classDecl = decl as ts.ClassDeclaration | ts.InterfaceDeclaration;
      if (!classDecl.members) {
        continue;
      }

      for (const memberDecl of classDecl.members) {
        if (ts.isSemicolonClassElement(memberDecl)) {
          this._diagnostics.push(JsiiDiagnostic.JSII_9996_UNNECESSARY_TOKEN.create(memberDecl));
          continue;
        }

        const member = ts.isConstructorDeclaration(memberDecl)
          ? getConstructor(this._typeChecker.getTypeAtLocation(memberDecl.parent))
          : ts.isIndexSignatureDeclaration(memberDecl)
          ? type.symbol.members?.get(ts.InternalSymbolName.Index) ??
            type.symbol.exports?.get(ts.InternalSymbolName.Index)
          : this._typeChecker.getSymbolAtLocation(ts.getNameOfDeclaration(memberDecl) ?? memberDecl);

        if (member && this._isPrivateOrInternal(member, memberDecl as ts.ClassElement)) {
          continue;
        }

        if (ts.isIndexSignatureDeclaration(memberDecl)) {
          // Index signatures (static or not) are not supported in the jsii type model.
          this._diagnostics.push(
            JsiiDiagnostic.JSII_1999_UNSUPPORTED.create(memberDecl, {
              what: 'Index signatures',
              suggestInternal: true,
            }),
          );
          continue;
        }

        if (!(declaringType.symbol.getDeclarations() ?? []).find((d) => d === memberDecl.parent)) {
          continue;
        }

        // constructors are handled later
        if (ts.isConstructorDeclaration(memberDecl)) {
          continue;
        }

        // eslint-disable-next-line no-await-in-loop
        if (ts.isMethodDeclaration(memberDecl) || ts.isMethodSignature(memberDecl)) {
          // eslint-disable-next-line no-await-in-loop
          this._visitMethod(member!, jsiiType, ctx.replaceStability(jsiiType.docs?.stability), classDecl);
        } else if (
          ts.isPropertyDeclaration(memberDecl) ||
          ts.isPropertySignature(memberDecl) ||
          ts.isAccessor(memberDecl)
        ) {
          // eslint-disable-next-line no-await-in-loop
          this._visitProperty(member!, jsiiType, ctx.replaceStability(jsiiType.docs?.stability), classDecl);
        } else {
          this._diagnostics.push(
            JsiiDiagnostic.JSII_9998_UNSUPPORTED_NODE.create(
              ts.getNameOfDeclaration(memberDecl) ?? memberDecl,
              memberDecl.kind,
            ),
          );
        }
        /* eslint-enable no-await-in-loop */
      }
    }

    const memberEmitContext = ctx.replaceStability(jsiiType.docs && jsiiType.docs.stability);

    // Find the first defined constructor in this class, or it's erased bases
    const constructor = [type, ...erasedBases].map(getConstructor).find((ctor) => ctor != null);
    const ctorDeclaration = constructor && (constructor.declarations?.[0] as ts.ConstructorDeclaration | undefined);
    if (constructor && ctorDeclaration) {
      const signature = this._typeChecker.getSignatureFromDeclaration(ctorDeclaration);

      if ((ts.getCombinedModifierFlags(ctorDeclaration) & ts.ModifierFlags.Private) === 0) {
        jsiiType.initializer = {
          locationInModule: this.declarationLocation(ctorDeclaration),
        };
        if (signature) {
          for (const param of signature.getParameters()) {
            jsiiType.initializer.parameters = jsiiType.initializer.parameters ?? [];
            jsiiType.initializer.parameters.push(
              // eslint-disable-next-line no-await-in-loop
              this._toParameter(param, ctx.replaceStability(jsiiType.docs?.stability)),
            );
            jsiiType.initializer.variadic = jsiiType.initializer?.parameters?.some((p) => !!p.variadic) || undefined;
            jsiiType.initializer.protected =
              (ts.getCombinedModifierFlags(ctorDeclaration) & ts.ModifierFlags.Protected) !== 0 || undefined;
          }
        }
        this._verifyConsecutiveOptionals(ctorDeclaration, jsiiType.initializer.parameters);
        jsiiType.initializer.docs = this._visitDocumentation(constructor, memberEmitContext).docs;
      }

      // Process constructor-based property declarations even if constructor is private
      if (signature) {
        for (const param of signature.getParameters()) {
          const decl = param.valueDeclaration ?? param.declarations?.[0];

          if (decl && ts.isParameterPropertyDeclaration(decl, decl.parent) && !this._isPrivateOrInternal(param)) {
            // eslint-disable-next-line no-await-in-loop
            this._visitProperty(param, jsiiType, memberEmitContext, ctorDeclaration.parent);
          }
        }
      }
    } else if (jsiiType.base) {
      this._deferUntilTypesAvailable(fqn, [jsiiType.base], type.symbol.valueDeclaration, (baseType) => {
        if (spec.isClassType(baseType)) {
          jsiiType.initializer = baseType.initializer;
        } else {
          this._diagnostics.push(
            JsiiDiagnostic.JSII_3999_INCOHERENT_TYPE_MODEL.create(
              type.symbol.valueDeclaration ?? type.symbol.declarations?.[0],
              `Base type of ${jsiiType.fqn} (${jsiiType.base}) is not a class`,
            ),
          );
        }
      });
    } else {
      jsiiType.initializer = {
        docs: ctx.stability && { stability: ctx.stability },
      };
    }

    this._verifyNoStaticMixing(jsiiType, type.symbol.valueDeclaration ?? type.symbol.declarations?.[0]);

    return _sortMembers(jsiiType);
  }