jsii
Version:
[](https://cdk.dev) [;
exports.Assembler = void 0;
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const spec = require("@jsii/spec");
const spec_1 = require("@jsii/spec");
const chalk = require("chalk");
const deepEqual = require("fast-deep-equal/es6");
const log4js = require("log4js");
const ts = require("typescript");
const Case = require("./case");
const symbol_id_1 = require("./common/symbol-id");
const directives_1 = require("./directives");
const docs_1 = require("./docs");
const jsii_diagnostic_1 = require("./jsii-diagnostic");
const literate = require("./literate");
const bindings = require("./node-bindings");
const reserved_words_1 = require("./reserved-words");
const deprecated_remover_1 = require("./transforms/deprecated-remover");
const deprecation_warnings_1 = require("./transforms/deprecation-warnings");
const runtime_info_1 = require("./transforms/runtime-info");
const utils_1 = require("./transforms/utils");
const validator_1 = require("./validator");
const version_1 = require("./version");
const warnings_1 = require("./warnings");
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const sortJson = require('sort-json');
const LOG = log4js.getLogger('jsii/assembler');
/**
* The JSII Assembler consumes a ``ts.Program`` instance and emits a JSII assembly.
*/
class Assembler {
/**
* @param projectInfo information about the package being assembled
* @param program the TypeScript program to be assembled from
* @param stdlib the directory where the TypeScript stdlib is rooted
*/
constructor(projectInfo, system, program, stdlib, options = {}) {
this.projectInfo = projectInfo;
this.system = system;
this.program = program;
this.stdlib = stdlib;
this._diagnostics = new Array();
this._deferred = new Array();
this._types = new Map();
this._packageInfoCache = new Map();
/** Map of Symbol to namespace export Symbol */
this._submoduleMap = new Map();
/**
* Submodule information
*
* Contains submodule information for all namespaces that have been seen
* across all assemblies (this and dependencies).
*
* Filtered to local submodules only at time of writing the assembly out to disk.
*/
this._submodules = new Map();
this._typeChecker = this.program.getTypeChecker();
if (options.stripDeprecated) {
let allowlistedDeprecations;
if (options.stripDeprecatedAllowListFile) {
if (!fs.existsSync(options.stripDeprecatedAllowListFile)) {
throw new Error(`--strip-deprecated file not found: ${options.stripDeprecatedAllowListFile}`);
}
allowlistedDeprecations = new Set(fs.readFileSync(options.stripDeprecatedAllowListFile, 'utf8').split('\n'));
}
this.deprecatedRemover = new deprecated_remover_1.DeprecatedRemover(this._typeChecker, allowlistedDeprecations);
}
if (options.addDeprecationWarnings) {
this.warningsInjector = new deprecation_warnings_1.DeprecationWarningsInjector(this._typeChecker);
}
this.compressAssembly = options.compressAssembly;
const dts = projectInfo.types;
let mainFile = dts.replace(/\.d\.ts(x?)$/, '.ts$1');
// If out-of-source build was configured (tsc's outDir and rootDir), the
// main file's path needs to be re-rooted from the outDir into the rootDir.
const tscOutDir = program.getCompilerOptions().outDir;
if (tscOutDir != null) {
mainFile = path.relative(tscOutDir, mainFile);
// rootDir may be set explicitly or not. If not, inferRootDir replicates
// tsc's behavior of using the longest prefix of all built source files.
this.tscRootDir = program.getCompilerOptions().rootDir ?? inferRootDir(program);
if (this.tscRootDir != null) {
mainFile = path.join(this.tscRootDir, mainFile);
}
}
this.mainFile = path.resolve(projectInfo.projectRoot, mainFile);
this.runtimeTypeInfoInjector = new runtime_info_1.RuntimeTypeInfoInjector(projectInfo.version);
}
get customTransformers() {
return (0, utils_1.combinedTransformers)(this.deprecatedRemover?.customTransformers ?? {}, this.runtimeTypeInfoInjector.makeTransformers(), this.warningsInjector?.customTransformers ?? {});
}
/**
* Attempt emitting the JSII assembly for the program.
*
* @return the result of the assembly emission.
*/
emit() {
if (!this.projectInfo.description) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0001_PKG_MISSING_DESCRIPTION.createDetached());
}
if (!this.projectInfo.homepage) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0002_PKG_MISSING_HOMEPAGE.createDetached());
}
const readme = _loadReadme.call(this);
if (readme == null) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0003_MISSING_README.createDetached());
}
const docs = _loadDocs.call(this);
const sourceFile = this.program.getSourceFile(this.mainFile);
if (sourceFile == null) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0004_COULD_NOT_FIND_ENTRYPOINT.createDetached(this.mainFile));
}
else {
this._registerDependenciesNamespaces(sourceFile);
if (LOG.isTraceEnabled()) {
LOG.trace(`Processing source file: ${chalk.blue(path.relative(this.projectInfo.projectRoot, sourceFile.fileName))}`);
}
const symbol = this._typeChecker.getSymbolAtLocation(sourceFile);
if (symbol) {
const moduleExports = this._typeChecker.getExportsOfModule(symbol);
moduleExports.map((item) => this._registerNamespaces(item, this.projectInfo.projectRoot));
for (const node of moduleExports) {
const decl = node.declarations?.[0];
if (decl == null) {
continue;
}
this._visitNode(decl, new EmitContext([], this.projectInfo.stability));
}
}
}
this.callDeferredsInOrder();
// Skip emitting if any diagnostic message is an error
if (this._diagnostics.find((diag) => diag.category === ts.DiagnosticCategory.Error) != null) {
LOG.debug('Skipping emit due to errors.');
try {
return { diagnostics: this._diagnostics, emitSkipped: true };
}
finally {
// Clearing ``this._diagnostics`` to allow contents to be garbage-collected.
this._afterEmit();
}
}
const jsiiVersion = this.projectInfo.jsiiVersionFormat === 'short' ? version_1.SHORT_VERSION : version_1.VERSION;
const assembly = {
schema: spec.SchemaVersion.LATEST,
name: this.projectInfo.name,
version: this.projectInfo.version,
description: this.projectInfo.description ?? this.projectInfo.name,
license: this.projectInfo.license,
keywords: this.projectInfo.keywords && Array.from(this.projectInfo.keywords),
homepage: this.projectInfo.homepage ?? this.projectInfo.repository.url,
author: this.projectInfo.author,
contributors: this.projectInfo.contributors && [...this.projectInfo.contributors],
repository: this.projectInfo.repository,
dependencies: noEmptyDict({
...this.projectInfo.dependencies,
...this.projectInfo.peerDependencies,
}),
dependencyClosure: noEmptyDict(toDependencyClosure(this.projectInfo.dependencyClosure)),
bundled: this.projectInfo.bundleDependencies,
types: Object.fromEntries(this._types),
submodules: noEmptyDict(toSubmoduleDeclarations(this.mySubmodules())),
targets: this.projectInfo.targets,
metadata: {
...this.projectInfo.metadata,
// Downstream consumers need this to map a symbolId in the outDir to a
// symbolId in the rootDir.
tscRootDir: this.tscRootDir,
},
docs,
readme,
jsiiVersion,
bin: this.projectInfo.bin,
fingerprint: '<TBD>',
};
if (this.deprecatedRemover) {
this._diagnostics.push(...this.deprecatedRemover.removeFrom(assembly));
}
if (this.warningsInjector) {
const jsiiMetadata = {
...(assembly.metadata?.jsii ?? {}),
...{ compiledWithDeprecationWarnings: true },
};
if (assembly.metadata) {
assembly.metadata.jsii = jsiiMetadata;
}
else {
assembly.metadata = { jsii: jsiiMetadata };
}
this.warningsInjector.process(assembly, this.projectInfo);
}
const validator = new validator_1.Validator(this.projectInfo, assembly);
const validationResult = validator.emit();
if (!validationResult.emitSkipped) {
const zipped = (0, spec_1.writeAssembly)(this.projectInfo.projectRoot, _fingerprint(assembly), {
compress: this.compressAssembly ?? false,
});
LOG.trace(`${zipped ? 'Zipping' : 'Emitting'} assembly: ${chalk.blue(path.join(this.projectInfo.projectRoot, spec_1.SPEC_FILE_NAME))}`);
}
try {
return {
diagnostics: [...this._diagnostics, ...validationResult.diagnostics],
emitSkipped: validationResult.emitSkipped,
};
}
finally {
this._afterEmit();
}
function _loadReadme() {
// Search for `README.md` in a case-insensitive way
const fileName = fs
.readdirSync(this.projectInfo.projectRoot)
.find((file) => file.toLocaleLowerCase() === 'readme.md');
if (fileName == null) {
return undefined;
}
const readmePath = path.join(this.projectInfo.projectRoot, fileName);
return loadAndRenderReadme(readmePath, this.projectInfo.projectRoot);
}
function _loadDocs() {
if (!this.projectInfo.stability && !this.projectInfo.deprecated) {
return undefined;
}
const deprecated = this.projectInfo.deprecated;
const stability = this.projectInfo.stability;
return { deprecated, stability };
}
}
_afterEmit() {
this._diagnostics = [];
this._deferred = [];
this._types.clear();
this._submoduleMap.clear();
this._submodules.clear();
this._packageInfoCache.clear();
}
/**
* Defer a callback until a (set of) types are available
*
* This is a helper function around _defer() which encapsulates the _dereference
* action (which is basically the majority use case for _defer anyway).
*
* Will not invoke the function with any 'undefined's; an error will already have been emitted in
* that case anyway.
*
* @param fqn FQN of the current type (the type that has a dependency on baseTypes)
* @param baseTypes Array of type references to be looked up
* @param referencingNode Node to report a diagnostic on if we fail to look up a t ype
* @param cb Callback to be invoked with the Types corresponding to the TypeReferences in baseTypes
*/
_deferUntilTypesAvailable(fqn, baseTypes, referencingNode, cb) {
// We can do this one eagerly
if (baseTypes.length === 0) {
cb();
return;
}
const baseFqns = baseTypes.map((bt) => (typeof bt === 'string' ? bt : bt.fqn));
this._defer(fqn, baseFqns, () => {
const resolved = baseFqns.map((x) => this._dereference(x, referencingNode)).filter((x) => x !== undefined);
if (resolved.length > 0) {
cb(...resolved);
}
});
}
/**
* Defer checks for after the program has been entirely processed; useful for verifying type references that may not
* have been discovered yet, and verifying properties about them.
*
* The callback is guaranteed to be executed only after all deferreds for all types in 'dependedFqns' have
* been executed.
*
* @param fqn FQN of the current type.
* @param dependedFqns List of FQNs of types this callback depends on. All deferreds for all
* @param cb the function to be called in a deferred way. It will be bound with ``this``, so it can depend on using
* ``this``.
*/
_defer(fqn, dependedFqns, cb) {
this._deferred.push({ fqn, dependedFqns, cb: cb.bind(this) });
}
/**
* Obtains the ``spec.Type`` for a given ``spec.NamedTypeReference``.
*
* @param ref the type reference to be de-referenced
*
* @returns the de-referenced type, if it was found, otherwise ``undefined``.
*/
_dereference(ref, referencingNode) {
if (typeof ref !== 'string') {
ref = ref.fqn;
}
const [assm] = ref.split('.');
let type;
if (assm === this.projectInfo.name) {
type = this._types.get(ref);
}
else {
const assembly = this.projectInfo.dependencyClosure.find((dep) => dep.name === assm);
type = assembly?.types?.[ref];
// since we are exposing a type of this assembly in this module's public API,
// we expect it to appear as a peer dependency instead of a normal dependency.
if (assembly) {
if (!(assembly.name in this.projectInfo.peerDependencies)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0005_MISSING_PEER_DEPENDENCY.create(referencingNode, // Cheating here for now, until the referencingNode can be made required
assembly.name, ref));
}
}
}
if (!type) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9002_UNRESOLVEABLE_TYPE.create(referencingNode, // Cheating here for now, until the referencingNode can be made required
ref));
}
return type;
}
/**
* Compute the JSII fully qualified name corresponding to a ``ts.Type`` instance. If for any reason a name cannot be
* computed for the type, a marker is returned instead, and an ``ts.DiagnosticCategory.Error`` diagnostic is
* inserted in the assembler context.
*
* @param type the type for which a JSII fully qualified name is needed.
* @param typeAnnotationNode the type annotation for which this FQN is generated. This is used for attaching the error
* marker. When there is no explicit type annotation (e.g: inferred method return type), the
* preferred substitute is the "type-inferred" element's name.
* @param typeUse the reason why this type was resolved (e.g: "return type")
* @param isThisType whether this type was specified or inferred as "this" or not
*
* @returns the FQN of the type, or some "unknown" marker.
*/
_getFQN(type, typeAnnotationNode, typeUse, isThisType) {
const sym = symbolFromType(type, this._typeChecker);
const typeDeclaration = sym.valueDeclaration ?? sym.declarations?.[0];
// Set to true to prevent further adding of Error diagnostics for known-bad reference
let hasError = false;
if (this._isPrivateOrInternal(sym)) {
// Check if this type is "this" (explicit or inferred method return type).
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3001_EXPOSED_INTERNAL_TYPE.create(typeAnnotationNode, sym, isThisType, typeUse).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here'));
hasError = true;
}
const tsName = this._typeChecker.getFullyQualifiedName(sym);
const groups = /^"([^"]+)"\.(.*)$/.exec(tsName);
if (!groups) {
if (!hasError) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3001_EXPOSED_INTERNAL_TYPE.create(typeAnnotationNode, sym, isThisType, typeUse).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here'));
hasError = true;
}
return tsName;
}
const [, modulePath, typeName] = groups;
const pkg = this.findPackageInfo(modulePath);
if (!pkg) {
if (!hasError) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9003_UNRESOLVEABLE_MODULE.create(typeAnnotationNode, modulePath).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here'));
hasError = true;
}
return `unknown.${typeName}`;
}
// If the symbol comes from an assembly whose submodules we've already
// spidered (or from the current assembly), look up there. This relies
// on an entry-point import of the library having been done first
// (`import * as x from 'module-root';`)
const submodule = this._submoduleMap.get(sym);
if (submodule != null) {
const submoduleNs = this._submodules.get(submodule).fqnResolutionPrefix;
return `${submoduleNs}.${typeName}`;
}
// This is the fallback: in case we can't find a symbolId for the given
// type, we're return this value. This is for backwards compatibility with
// modules that haven't been compiled to have symbolId support. Those also
// most likely won't be using submodules so this legacy guess will be correct.
const fallbackFqn = `${pkg.name}.${typeName}`;
// If the type is coming from the current module, we won't find it in a dependency
if (pkg.name === this.projectInfo.name) {
return fallbackFqn;
}
// Otherwise look up the symbol identifier in the dependency assemblies
// This is now the preferred mechanism but we can't do this as the only mechanism,
// as we may still have compile against very old assemblies that don't have a
// symbol identifier table at all.
const dep = this.projectInfo.dependencyClosure.find((d) => d.name === pkg.name);
if (!dep) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9000_UNKNOWN_MODULE.create(typeAnnotationNode, pkg.name));
return fallbackFqn;
}
const symbolId = (0, symbol_id_1.symbolIdentifier)(this._typeChecker, sym, {
assembly: dep,
});
const fqn = (dep && symbolId ? symbolIdIndex(dep)[symbolId] : undefined) ?? fallbackFqn;
if (!fqn || !this._dereference({ fqn }, sym.valueDeclaration)) {
if (!hasError) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3002_USE_OF_UNEXPORTED_FOREIGN_TYPE.create(typeAnnotationNode, fqn ?? tsName, typeUse, pkg).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here'));
hasError = true;
}
}
return fqn;
}
/**
* For all modules in the dependency closure, crawl their exports to register
* the submodules they contain.
*
* @param entryPoint the main source file for the currently compiled module.
*/
_registerDependenciesNamespaces(entryPoint) {
for (const assm of this.projectInfo.dependencyClosure) {
const resolved = ts.resolveModuleName(assm.name, entryPoint.fileName, this.program.getCompilerOptions(), ts.sys);
// If we can't resolve the module name, simply ignore it (TypeScript compilation likely failed)
if (resolved.resolvedModule == null) {
continue;
}
const source = this.program.getSourceFile(resolved.resolvedModule.resolvedFileName);
const depMod = source && this._typeChecker.getSymbolAtLocation(source);
// It's unlikely, but if we can't get the SourceFile here, ignore it (TypeScript compilation probably failed)
if (depMod == null) {
continue;
}
const depRoot = packageRoot(resolved.resolvedModule.resolvedFileName);
for (const symbol of this._typeChecker.getExportsOfModule(depMod)) {
this._registerNamespaces(symbol, depRoot);
}
}
function packageRoot(file) {
const parent = path.dirname(file);
if (path.basename(parent) === 'node_modules' || parent === file) {
return file;
}
return packageRoot(parent);
}
}
_registerNamespaces(symbol, packageRoot) {
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
if (declaration == null) {
// Nothing to do here...
return;
}
if (ts.isModuleDeclaration(declaration)) {
// Looks like:
//
// export some_namespace {
// ...
// }
//
// No way to configure targets
const { fqn, fqnResolutionPrefix } = qualifiedNameOf.call(this, symbol, true);
this._submodules.set(symbol, {
fqn,
fqnResolutionPrefix,
symbolId: (0, symbol_id_1.symbolIdentifier)(this._typeChecker, symbol),
locationInModule: this.declarationLocation(declaration),
});
this._addToSubmodule(symbol, symbol, packageRoot);
return;
}
if (!ts.isNamespaceExport(declaration)) {
// Nothing to do here...
return;
}
const moduleSpecifier = declaration.parent.moduleSpecifier;
if (moduleSpecifier == null || !ts.isStringLiteral(moduleSpecifier)) {
// There is a grammar error here, so we'll let tsc report this for us.
return;
}
const resolution = ts.resolveModuleName(moduleSpecifier.text, declaration.getSourceFile().fileName, this.program.getCompilerOptions(), this.system);
if (resolution.resolvedModule == null) {
// Unresolvable module... We'll let tsc report this for us.
return;
}
if (
// We're not looking into a dependency's namespace exports, and the resolution says it's external
(packageRoot === this.projectInfo.projectRoot && resolution.resolvedModule.isExternalLibraryImport) ||
// Or the module resolves outside of the current dependency's tree entirely
!isUnder(resolution.resolvedModule.resolvedFileName, packageRoot) ||
// Or the module is under one the current dependency's node_modules subtree
resolution.resolvedModule.resolvedFileName
.split('/') // Separator is always '/', even on Windows
.filter((entry) => entry === 'node_modules').length !==
packageRoot.split('/').filter((entry) => entry === 'node_modules').length) {
// External re-exports are "pure-javascript" sugar; they need not be
// represented in the jsii Assembly since the types in there will be
// resolved through dependencies.
return;
}
const sourceFile = this.program.getSourceFile(resolution.resolvedModule.resolvedFileName);
const sourceModule = this._typeChecker.getSymbolAtLocation(sourceFile);
// If there's no module, it's a syntax error, and tsc will have reported it for us.
if (sourceModule) {
if (symbol.name !== Case.camel(symbol.name) && symbol.name !== Case.snake(symbol.name)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8004_SUBMOULE_NAME_CASING.create(declaration.name, symbol.name));
}
const { fqn, fqnResolutionPrefix } = qualifiedNameOf.call(this, symbol);
const targets = loadSubmoduleTargetConfig(sourceFile.fileName);
// There is no need to process the README file for submodules that are
// external (i.e: from a dependency), as these will not be emitted in the
// assembly. That'd be wasted effort, and could fail if the README file
// refers to literate examples that are not packaged in the dependency.
const readme = packageRoot === this.projectInfo.projectRoot
? loadSubmoduleReadMe(sourceFile.fileName, this.projectInfo.projectRoot)
: undefined;
this._submodules.set(symbol, {
fqn,
fqnResolutionPrefix,
targets,
readme,
symbolId: (0, symbol_id_1.symbolIdentifier)(this._typeChecker, symbol),
locationInModule: this.declarationLocation(declaration),
});
this._addToSubmodule(symbol, sourceModule, packageRoot);
}
function qualifiedNameOf(sym, inlineNamespace = false) {
if (this._submoduleMap.has(sym)) {
const parent = this._submodules.get(this._submoduleMap.get(sym));
const fqn = `${parent.fqn}.${sym.name}`;
return {
fqn,
fqnResolutionPrefix: inlineNamespace ? parent.fqnResolutionPrefix : fqn,
};
}
const symbolLocation = sym.getDeclarations()?.[0]?.getSourceFile()?.fileName;
const pkgInfo = symbolLocation ? this.findPackageInfo(symbolLocation) : undefined;
const assemblyName = pkgInfo?.name ?? this.projectInfo.name;
const fqn = `${assemblyName}.${sym.name}`;
return {
fqn,
fqnResolutionPrefix: inlineNamespace ? this.projectInfo.name : fqn,
};
}
function loadSubmoduleTargetConfig(submoduleMain) {
const jsiirc = path.resolve(submoduleMain, '..', '.jsiirc.json');
if (!fs.existsSync(jsiirc)) {
return undefined;
}
const data = JSON.parse(fs.readFileSync(jsiirc, 'utf-8'));
return data.targets;
}
/**
* Load the README for the given submodule
*
* If the submodule is loaded from a complete directory (determined by the 'main'
* file ending in `index.[d.]ts`, then we load `README.md` in that same directory.
*
* If the submodule is loaded from a file, like `mymodule.[d.]ts`, we will load
* `mymodule.README.md`.
*/
function loadSubmoduleReadMe(submoduleMain, projectRoot) {
const fileBase = path.basename(submoduleMain).replace(/(\.d)?\.ts$/, '');
const readMeName = fileBase === 'index' ? 'README.md' : `${fileBase}.README.md`;
const fullPath = path.join(path.dirname(submoduleMain), readMeName);
return loadAndRenderReadme(fullPath, projectRoot);
}
}
/**
* Registers Symbols to a particular submodule. This is used to associate
* declarations exported by an `export * as ns from 'moduleLike';` statement
* so that they can subsequently be correctly namespaced.
*
* @param ns the symbol that identifies the submodule.
* @param moduleLike the module-like symbol bound to the submodule.
* @param packageRoot the root of the package being traversed.
*/
_addToSubmodule(ns, moduleLike, packageRoot) {
// For each symbol exported by the moduleLike, map it to the ns submodule.
for (const symbol of this._typeChecker.getExportsOfModule(moduleLike)) {
if (this._submoduleMap.has(symbol)) {
const currNs = this._submoduleMap.get(symbol);
// Checking if there's been two submodules exporting the same symbol,
// which is illegal. We can tell if the currently registered symbol has
// a different name than the one we're currently trying to register in.
if (currNs.name !== ns.name) {
const currNsDecl = currNs.valueDeclaration ?? currNs.declarations?.[0];
const nsDecl = ns.valueDeclaration ?? ns.declarations?.[0];
// Make sure the error message always lists causes in the same order
const refs = [
{ decl: currNsDecl, name: currNs.name },
{ decl: nsDecl, name: ns.name },
].sort(({ name: l }, { name: r }) => l.localeCompare(r));
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3003_SYMBOL_IS_EXPORTED_TWICE.create(_nameOrDeclarationNode(symbol), refs[0].name, refs[1].name)
.addRelatedInformationIf(refs[0].decl, `Symbol is exported under the "${refs[0].name}" submodule`)
.addRelatedInformationIf(refs[1].decl, `Symbol is exported under the "${refs[1].name}" submodule`));
}
// Found two re-exports, which is odd, but they use the same submodule,
// so it's probably okay? That's likely a tsc error, which will have
// been reported for us already anyway.
continue;
}
this._submoduleMap.set(symbol, ns);
// If the exported symbol has any declaration, and that delcaration is of
// an entity that can have nested declarations of interest to jsii
// (classes, interfaces, enums, modules), we need to also associate those
// nested symbols to the submodule (or they won't be named correctly!)
const decl = symbol.declarations?.[0];
if (decl != null) {
if (ts.isClassDeclaration(decl) || ts.isInterfaceDeclaration(decl) || ts.isEnumDeclaration(decl)) {
const type = this._typeChecker.getTypeAtLocation(decl);
if (isSingleValuedEnum(type, this._typeChecker)) {
// type.symbol !== symbol, because symbol is the enum itself, but
// since it's single-valued, the TypeChecker will only show us the
// value's symbol later on.
this._submoduleMap.set(type.symbol, ns);
}
if (type.symbol.exports) {
// eslint-disable-next-line no-await-in-loop
this._addToSubmodule(ns, symbol, packageRoot);
}
}
else if (ts.isModuleDeclaration(decl)) {
// eslint-disable-next-line no-await-in-loop
this._registerNamespaces(symbol, packageRoot);
}
else if (ts.isNamespaceExport(decl)) {
// eslint-disable-next-line no-await-in-loop
this._registerNamespaces(symbol, packageRoot);
}
}
}
}
/**
* Register exported types in ``this.types``.
*
* @param node a node found in a module
* @param namePrefix the prefix for the types' namespaces
*/
// eslint-disable-next-line complexity
_visitNode(node, context) {
if (ts.isNamespaceExport(node)) {
// export * as ns from 'module';
// Note: the "ts.NamespaceExport" refers to the "export * as ns" part of
// the statement only. We must refer to `node.parent` in order to be able
// to access the module specifier ("from 'module'") part.
const symbol = this._typeChecker.getSymbolAtLocation(node.parent.moduleSpecifier);
if (LOG.isTraceEnabled()) {
LOG.trace(`Entering submodule: ${chalk.cyan([...context.namespace, symbol.name].join('.'))}`);
}
const nsContext = context.appendNamespace(node.name.text);
const allTypes = this._typeChecker.getExportsOfModule(symbol).flatMap((child) => {
const decl = child.declarations?.[0];
if (decl == null) {
return [];
}
return this._visitNode(decl, nsContext);
});
if (LOG.isTraceEnabled()) {
LOG.trace(`Leaving submodule: ${chalk.cyan([...context.namespace, symbol.name].join('.'))}`);
}
return allTypes;
}
if (ts.isExportSpecifier(node)) {
// This is what happens when one does `export { Symbol } from "./location";`
// ExportSpecifier: ~~~~~~
const resolvedSymbol = this._typeChecker.getExportSpecifierLocalTargetSymbol(node);
const decl = resolvedSymbol?.valueDeclaration ?? resolvedSymbol?.declarations?.[0];
if (!decl) {
// A grammar error, compilation will already have failed
return [];
}
return this._visitNode(decl, context);
}
if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) === 0) {
return [];
}
let jsiiType;
if (ts.isClassDeclaration(node) && _isExported(node)) {
// export class Name { ... }
this._validateHeritageClauses(node.heritageClauses);
jsiiType = this._visitClass(this._typeChecker.getTypeAtLocation(node), context);
if (jsiiType) {
this.registerExportedClassFqn(node, jsiiType.fqn);
}
}
else if (ts.isInterfaceDeclaration(node) && _isExported(node)) {
// export interface Name { ... }
this._validateHeritageClauses(node.heritageClauses);
jsiiType = this._visitInterface(this._typeChecker.getTypeAtLocation(node), context);
}
else if (ts.isEnumDeclaration(node) && _isExported(node)) {
// export enum Name { ... }
jsiiType = this._visitEnum(this._typeChecker.getTypeAtLocation(node), context);
}
else if (ts.isModuleDeclaration(node)) {
// export namespace name { ... }
const name = node.name.getText();
const symbol = this._typeChecker.getSymbolAtLocation(node.name);
if (LOG.isTraceEnabled()) {
LOG.trace(`Entering namespace: ${chalk.cyan([...context.namespace, name].join('.'))}`);
}
const nsContext = context.appendNamespace(node.name.getText());
const allTypes = this._typeChecker.getExportsOfModule(symbol).flatMap((prop) => {
const decl = prop.declarations?.[0];
if (decl == null) {
return [];
}
return this._visitNode(decl, nsContext);
});
if (LOG.isTraceEnabled()) {
LOG.trace(`Leaving namespace: ${chalk.cyan([...context.namespace, name].join('.'))}`);
}
return allTypes;
}
else {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9998_UNSUPPORTED_NODE.create(ts.getNameOfDeclaration(node) ?? node, node.kind));
}
if (!jsiiType) {
return [];
}
// If symbolId hasn't been set yet, set it here
if (!jsiiType.symbolId) {
jsiiType.symbolId = this.getSymbolId(node);
}
// Let's quickly verify the declaration does not collide with a submodule. Submodules get case-adjusted for each
// target language separately, so names cannot collide with case-variations.
for (const submodule of this._submodules.keys()) {
const candidates = Array.from(new Set([submodule.name, Case.camel(submodule.name), Case.pascal(submodule.name), Case.snake(submodule.name)]));
const colliding = candidates.find((name) => `${this.projectInfo.name}.${name}` === jsiiType.fqn);
if (colliding != null) {
const submoduleDeclName = _nameOrDeclarationNode(submodule);
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5011_SUBMODULE_NAME_CONFLICT.create(ts.getNameOfDeclaration(node) ?? node, submodule.name, jsiiType.name, candidates).addRelatedInformationIf(submoduleDeclName, 'This is the conflicting submodule declaration'));
}
}
if (LOG.isInfoEnabled()) {
LOG.info(`Registering JSII ${chalk.magenta(jsiiType.kind)}: ${chalk.green(jsiiType.fqn)}`);
}
this._types.set(jsiiType.fqn, jsiiType);
jsiiType.locationInModule = this.declarationLocation(node);
const type = this._typeChecker.getTypeAtLocation(node);
if (type.symbol.exports) {
const nestedContext = context.appendNamespace(type.symbol.name);
const visitedNodes = this._typeChecker
.getExportsOfModule(type.symbol)
.filter((s) => s.declarations)
.flatMap((exportedNode) => {
const decl = exportedNode.valueDeclaration ?? exportedNode.declarations?.[0];
if (decl == null) {
return [];
}
return [this._visitNode(decl, nestedContext)];
});
for (const nestedTypes of visitedNodes) {
for (const nestedType of nestedTypes) {
if (nestedType.namespace !== nestedContext.namespace.join('.')) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5012_NAMESPACE_IN_TYPE.create(ts.getNameOfDeclaration(node) ?? node, jsiiType.fqn, nestedType.namespace));
}
}
}
}
return [jsiiType];
}
getSymbolId(node) {
return (0, symbol_id_1.symbolIdentifier)(this._typeChecker, this._typeChecker.getTypeAtLocation(node).symbol);
}
_validateHeritageClauses(clauses) {
if (clauses == null || clauses.length === 0) {
// Nothing to do.
return;
}
for (const clause of clauses) {
for (const node of clause.types) {
const parentType = this._typeChecker.getTypeAtLocation(node);
if (parentType.symbol == null) {
// The parent type won't have a symbol if it's an "error type" inserted by the type checker when the original
// code contains a compilation error. In such cases, the TypeScript compiler will already have reported about
// the incoherent declarations, so we'll just not re-validate it there (we'd fail anyway).
continue;
}
// For some reason, we cannot trust parentType.isClassOrInterface()
const badDecl = parentType.symbol.declarations?.find((decl) => !ts.isClassDeclaration(decl) && // <-- local classes
!ts.isInterfaceDeclaration(decl) && // <-- local interfaces
!ts.isModuleDeclaration(decl));
if (badDecl != null) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3004_INVALID_SUPERTYPE.create(node, clause, badDecl).addRelatedInformation(badDecl, 'The invalid super type is declared here.'));
}
}
}
}
declarationLocation(node) {
const file = node.getSourceFile();
const line = ts.getLineAndCharacterOfPosition(file, node.getStart()).line;
const filename = path.normalize(path.relative(this.projectInfo.projectRoot, file.fileName)).replace(/\\/g, '/');
return {
filename,
line: line + 1,
};
}
_processBaseInterfaces(fqn, baseTypes) {
const erasedBases = new Array();
if (!baseTypes) {
return { erasedBases };
}
const result = new Array();
const baseInterfaces = new Set();
const processBaseTypes = (types) => {
for (const iface of types) {
// base is private/internal, so we continue recursively with it's own bases
if (this._isPrivateOrInternal(iface.symbol) || isInternalSymbol(iface.symbol)) {
erasedBases.push(iface);
if (!isInternalSymbol(iface.symbol)) {
const bases = iface.getBaseTypes();
if (bases) {
processBaseTypes(bases);
}
}
continue;
}
baseInterfaces.add(iface);
}
};
processBaseTypes(baseTypes);
const typeRefs = Array.from(baseInterfaces).map((iface) => {
const decl = iface.symbol.valueDeclaration;
const typeRef = this._typeReference(iface, decl, 'base interface');
return { decl, typeRef };
});
for (const { decl, typeRef } of typeRefs) {
if (!spec.isNamedTypeReference(typeRef)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3005_TYPE_USED_AS_INTERFACE.create(decl, typeRef));
continue;
}
this._deferUntilTypesAvailable(fqn, [typeRef], decl, (deref) => {
if (!spec.isInterfaceType(deref)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3005_TYPE_USED_AS_INTERFACE.create(decl, typeRef));
}
});
result.push(typeRef);
}
return {
interfaces: result.length === 0 ? undefined : result,
erasedBases,
};
}
// eslint-disable-next-line complexity
_visitClass(type, ctx) {
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(jsii_diagnostic_1.JsiiDiagnostic.JSII_8000_PASCAL_CASED_TYPE_NAMES.create(type.symbol.valueDeclaration.name ??
type.symbol.valueDeclaration ??
type.symbol.declarations?.[0], type.symbol.name));
}
const classDeclaration = type.symbol.valueDeclaration;
for (const typeParam of classDeclaration.typeParameters ?? []) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1006_GENERIC_TYPE.create(typeParam));
}
const jsiiType = 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();
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(jsii_diagnostic_1.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(jsii_diagnostic_1.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();
for (const heritage of [type, ...erasedBases].map((t) => t.symbol.valueDeclaration.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(jsii_diagnostic_1.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();
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(jsii_diagnostic_1.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 = (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;
if (!classDecl.members) {
continue;
}
for (const memberDecl of classDecl.members) {
if (ts.isSemicolonClassElement(memberDecl)) {
this._diagnostics.push(jsii_diagnostic_1.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)) {
continue;
}
if (ts.isIndexSignatureDeclaration(memberDecl)) {
// Index signatures (static or not) are not supported in the jsii type model.
this._diagnostics.push(jsii_diagnostic_1.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(jsii_diagnostic_1.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];
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(jsii_diagnostic_1.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);
}
/**
* Use the TypeChecker's getTypeFromTypeNode, but throw a descriptive error if it fails
*/
_getTypeFromTypeNode(t) {
const type = this._typeChecker.getTypeFromTypeNode(t);
if (isErrorType(type)) {
throw new Error(`Unable to resolve type: ${t.getFullText()}. This typically happens if something is wrong with your dependency closure.`);
}
return type;
}
/**
* Check that this class doesn't declare any members that are of different staticness in itself or any of its bases
*/
_verifyNoStaticMixing(klass, decl) {
// Check class itself--may have two methods/props with the same name, so check the arrays
const statics = new Set((klass.methods ?? [])
.concat(klass.properties ?? [])
.filter((x) => x.static)
.map((x) => x.name));
const nonStatics = new Set((klass.methods ?? [])
.concat(klass.properties ?? [])
.filter((x) => !x.static)
.map((x) => x.name));
// Intersect
for (const member of intersect(statics, nonStatics)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5013_STATIC_INSTANCE_CONFLICT.create(decl, member, klass));
}
// Check against base classes. They will not contain duplicate member names so we can load
// the members into a map.
const classMembers = typeMembers(klass);
this._withBaseClass(klass, decl, (base, recurse) => {
for (const [name, baseMember] of Object.entries(typeMembers(base))) {
const member = classMembers[name];
if (!member) {
continue;
}
if (!!baseMember.static !== !!member.static) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5014_INHERITED_STATIC_CONFLICT.create(decl, member, klass, baseMember, base));
}
}
recurse();
});
}
/**
* Wrapper around _deferUntilTypesAvailable, invoke the callback with the given classes' base type
*
* Does nothing if the given class doesn't have a base class.
*
* The second argument will be a `recurse` function for easy recursion up the inheritance tree
* (no messing around with binding 'self' and 'this' and doing multiple calls to _withBaseClass.)
*/
_withBaseClass(klass, decl, cb) {
if (klass.base) {
this._deferUntilTypesAvailable(klass.fqn, [klass.base], decl, (base) => {
if (!spec.isClassType(base)) {
throw new Error('Oh no');
}
cb(base, () => this._withBaseClass(base, decl, cb));
});
}
}
/**
* @returns true if this member is internal and should be omitted from the type manifest
*/
_isPrivateOrInternal(symbol, validateDeclaration) {
const hasInternalJsDocTag = _hasInternalJsDocTag(symbol);
const hasInternalSymbolName = isInternalSymbol(symbol);
const hasUnderscorePrefix = !hasInternalSymbolName && symbol.name.startsWith('_');
if (_isPrivate(symbol)) {
LOG.trace(`${chalk.cyan(symbol.name)} is marked "private", or is an unexported type declaration`);
return true;
}
// If all the declarations are marked with `@jsii ignore`, then this is effetcively private as far as jsii is concerned.
if (symbol.declarations?.every((decl) => directives_1.Directives.of(decl, (diag) => this._diagnostics.push(diag)).ignore != null)) {
return true;
}
if (!hasInternalJsDocTag && !hasUnderscorePrefix) {
return false;
}
// We only validate if we have a declaration and the symbol doesn't have an internal name.
if (validateDeclaration && !hasInternalSymbolName) {
if (!hasUnderscorePrefix) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8005_INTERNAL_UNDERSCORE.create(ts.getNameOfDeclaration(validateDeclaration) ?? validateDeclaration, symbol.name));
}
if (!hasInternalJsDocTag) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8006_UNDERSCORE_INTERNAL.create(ts.getNameOfDeclaration(validateDeclaration) ?? validateDeclaration, symbol.name));
}
}
return true;
}
_visitEnum(type, ctx) {
if (LOG.isTraceEnabled()) {
LOG.trace(`Processing enum: ${chalk.gray(ctx.namespace.join('.'))}.${chalk.cyan(type.symbol.name)}`);
}
// Forcefully resolving to the EnumDeclaration symbol for single-valued enums
let decl = type.symbol.declarations?.[0];
let symbol;
if (decl && ts.isEnumMember(decl)) {
decl = decl?.parent;
}
if (decl && ts.isEnumDeclaration(decl)) {
symbol = getSymbolFromDeclaration(decl, this._typeChecker);
}
if (!decl || !symbol || !ts.isEnumDeclaration(decl)) {
throw new Error(`Unable to resolve enum declaration for ${type.symbol.name}!`);
}
if (_hasInternalJsDocTag(symbol)) {
return undefined;
}
// check the enum to see if there are duplicate enum values
this.assertNoDuplicateEnumValues(decl);
this._warnAboutReservedWords(symbol);
const flags = ts.getCombinedModifierFlags(decl);
if (flags & ts.ModifierFlags.Const) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1000_NO_CONST_ENUM.create(decl.modifiers?.find((mod) => mod.kind === ts.SyntaxKind.ConstKeyword) ?? decl));
}
const { docs } = this._visitDocumentation(symbol, ctx);
const typeContext = ctx.replaceStability(docs?.stability);
const members = type.isUnion() ? type.types : [type];
if (Case.pascal(symbol.name) !== symbol.name) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8000_PASCAL_CASED_TYPE_NAMES.create(symbol.valueDeclaration.name, symbol.name));
}
const jsiiType = bindings.setEnumRelatedNode({
assembly: this.projectInfo.name,
fqn: `${[this.projectInfo.name, ...ctx.namespace].join('.')}.${symbol.name}`,
kind: spec.TypeKind.Enum,
members: members.map((m) => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { docs } = this._visitDocumentation(m.symbol, typeContext);
return { name: m.symbol.name, docs };
}),
name: symbol.name,
namespace: ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined,
docs,
// Set SymbolId here instead of later, as by default TS will pick single-enum members
// as the target symbol if possible.
symbolId: (0, symbol_id_1.symbolIdentifier)(this._typeChecker, symbol),
}, decl);
return jsiiType;
}
assertNoDuplicateEnumValues(decl) {
const enumValues = decl.members
.filter((m) => m.initializer)
.map((member) => {
return {
value: member.initializer.getText(),
name: member.name.getText(),
decl: ts.getNameOfDeclaration(member),
};
});
const hasDuplicateEnumValues = enumValues.some((val, _, arr) => arr.filter((e) => val.value === e.value).length > 1);
if (hasDuplicateEnumValues) {
const enumValueMap = enumValues.reduce((acc, val) => {
if (!acc[val.value]) {
acc[val.value] = [];
}
acc[val.value].push(val);
return acc;
}, {});
for (const duplicateValue of Object.keys(enumValueMap)) {
if (enumValueMap[duplicateValue].length > 1) {
const err = jsii_diagnostic_1.JsiiDiagnostic.JSII_1004_DUPLICATE_ENUM_VALUE.create(enumValueMap[duplicateValue][0].decl, duplicateValue, enumValueMap[duplicateValue].map((v) => v.name));
for (let i = 1; i < enumValueMap[duplicateValue].length; i++) {
err.addRelatedInformation(enumValueMap[duplicateValue][i].decl, 'The conflicting declaration is here');
}
this._diagnostics.push(err);
}
}
}
}
/**
* Return docs for a symbol
*/
_visitDocumentation(sym, context) {
const result = (0, docs_1.parseSymbolDocumentation)(sym, this._typeChecker);
for (const diag of result.diagnostics ?? []) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_7999_DOCUMENTATION_ERROR.create(sym.valueDeclaration ?? sym.declarations?.[0], diag));
}
const decl = sym.valueDeclaration ?? sym.declarations?.[0];
// The @struct hint is only valid for interface declarations
if (decl && !ts.isInterfaceDeclaration(decl) && result.hints.struct) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_7001_ILLEGAL_HINT.create(_findHint(decl, 'struct'), 'struct', 'interfaces with only readonly properties').addRelatedInformationIf(ts.getNameOfDeclaration(decl) ?? decl, 'The annotated declaration is here'));
// Clean up the bad hint...
delete result.hints.struct;
}
// Apply the current context's stability if none was specified locally.
if (result.docs.stability == null) {
result.docs.stability = context.stability;
}
const allUndefined = Object.values(result.docs).every((v) => v === undefined);
return {
docs: !allUndefined ? result.docs : undefined,
hints: result.hints,
};
}
/**
* Check that all parameters the doc block refers to with a @param declaration actually exist
*/
_validateReferencedDocParams(method, methodSym) {
const params = (0, docs_1.getReferencedDocParams)(methodSym);
const actualNames = new Set((method.parameters ?? []).map((p) => p.name));
for (const param of params) {
if (!actualNames.has(param)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_7000_NON_EXISTENT_PARAMETER.create(methodSym.valueDeclaration ?? methodSym.declarations?.[0], method, param));
}
}
}
_visitInterface(type, ctx) {
if (LOG.isTraceEnabled()) {
LOG.trace(`Processing interface: ${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}`;
const { docs, hints } = this._visitDocumentation(type.symbol, ctx);
const jsiiType = bindings.setInterfaceRelatedNode({
assembly: this.projectInfo.name,
fqn,
kind: spec.TypeKind.Interface,
name: type.symbol.name,
namespace: ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined,
docs,
}, type.symbol.declarations?.[0]);
const { interfaces, erasedBases } = this._processBaseInterfaces(fqn, type.getBaseTypes());
jsiiType.interfaces = apply(interfaces, (arr) => arr.map((i) => i.fqn));
const typeDecl = (type.symbol.valueDeclaration ?? type.symbol.declarations?.[0]);
for (const typeParam of typeDecl?.typeParameters ?? []) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1006_GENERIC_TYPE.create(typeParam));
}
for (const decl of typeDecl?.members?.filter((mem) => ts.isIndexSignatureDeclaration(mem)) ?? []) {
const sym = type.symbol.members?.get(ts.InternalSymbolName.Index) ?? type.symbol.exports?.get(ts.InternalSymbolName.Index);
if (sym != null && this._isPrivateOrInternal(sym, decl)) {
continue;
}
// Index signatures (static or not) are not supported in the jsii type model.
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1999_UNSUPPORTED.create(decl, { what: 'Index signatures', suggestInternal: true }));
}
for (const declaringType of [type, ...erasedBases]) {
for (const member of declaringType.getProperties()) {
const decl = member.valueDeclaration ?? member.declarations?.[0];
if (!(declaringType.symbol.getDeclarations() ?? []).find((d) => d === decl?.parent)) {
continue;
}
if (this._isPrivateOrInternal(member, decl)) {
continue;
}
if (decl && (ts.isMethodDeclaration(decl) || ts.isMethodSignature(decl))) {
// eslint-disable-next-line no-await-in-loop
this._visitMethod(member, jsiiType, ctx.replaceStability(jsiiType.docs?.stability), typeDecl);
}
else if (decl && (ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl) || ts.isAccessor(decl))) {
// eslint-disable-next-line no-await-in-loop
this._visitProperty(member, jsiiType, ctx.replaceStability(jsiiType.docs?.stability), typeDecl);
}
else {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9998_UNSUPPORTED_NODE.create(_nameOrDeclarationNode(member), (member.valueDeclaration ?? member.declarations?.[0])?.kind ?? ts.SyntaxKind.Unknown));
}
}
}
// Calculate datatype based on the datatypeness of this interface and all of its parents
// To keep the spec minimal the actual values of the attribute are "true" or "undefined" (to represent "false").
const declaration = type.symbol.valueDeclaration ?? type.symbol.declarations?.[0];
this._deferUntilTypesAvailable(fqn, jsiiType.interfaces ?? [], declaration, (...bases) => {
if ((jsiiType.methods ?? []).length === 0) {
jsiiType.datatype = true;
}
else if (hints.struct) {
this._diagnostics.push(jsiiType.methods.reduce((diag, mthod) => {
const node = bindings.getMethodRelatedNode(mthod);
return node
? diag.addRelatedInformation(ts.getNameOfDeclaration(node) ?? node, 'A method is declared here')
: diag;
}, jsii_diagnostic_1.JsiiDiagnostic.JSII_7001_ILLEGAL_HINT.create(declaration && _findHint(declaration, 'struct'), 'struct', 'interfaces with only readonly properties').addRelatedInformationIf(ts.getNameOfDeclaration(declaration) ?? declaration, 'The annotated declartion is here')));
}
for (const base of bases) {
if (spec.isInterfaceType(base) && !base.datatype) {
jsiiType.datatype = undefined;
}
}
const interfaceName = isInterfaceName(jsiiType.name);
// If it's not a datatype the name must start with an "I".
if (!jsiiType.datatype && !interfaceName) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8007_BEHAVIORAL_INTERFACE_NAME.create(ts.getNameOfDeclaration(declaration) ?? declaration, jsiiType.name));
}
// NOTE: We need to be careful with the `I` prefix for behavioral interfaces, as this can mess with PascalCase
// transformations, especially with short names such as `IA`, ...
const expectedName = interfaceName ? `I${Case.pascal(type.symbol.name.slice(1))}` : Case.pascal(type.symbol.name);
if (expectedName !== type.symbol.name) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8000_PASCAL_CASED_TYPE_NAMES.create(type.symbol.declarations?.[0]?.name, type.symbol.name, expectedName));
}
// If the name starts with an "I" it is not intended as a datatype, so switch that off,
// unless a TSDoc hint was set to force this to be considered a behavioral interface.
if (jsiiType.datatype && interfaceName && !hints.struct) {
delete jsiiType.datatype;
}
// Okay, this is a data type, check that all properties are readonly
if (jsiiType.datatype) {
for (const prop of jsiiType.properties ?? []) {
if (!prop.immutable) {
const p = type.getProperty(prop.name);
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3008_STRUCT_PROPS_MUST_BE_READONLY.create(_nameOrDeclarationNode(p), p.name, jsiiType));
// force property to be "readonly" since jsii languages will pass this by-value
prop.immutable = true;
}
}
}
else {
// This is *NOT* a data type, so it may not extend something that is one.
for (const base of bases) {
if (!spec.isInterfaceType(base)) {
// Invalid type we already warned about earlier, just ignoring it here..
continue;
}
if (base.datatype) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3007_ILLEGAL_STRUCT_EXTENSION.create(type.symbol.valueDeclaration ?? type.symbol.declarations?.[0], jsiiType, base));
}
}
}
});
// Check that no interface declares a member that's already declared
// in a base type (not allowed in C#).
const names = memberNames(jsiiType);
const checkNoIntersection = (...bases) => {
for (const base of bases) {
if (!spec.isInterfaceType(base)) {
continue;
}
const baseMembers = memberNames(base);
for (const memberName of names) {
if (baseMembers.includes(memberName)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5015_REDECLARED_INTERFACE_MEMBER.create(type.symbol.valueDeclaration ?? type.symbol.declarations?.[0], memberName, jsiiType));
}
}
// Recurse upwards
this._deferUntilTypesAvailable(fqn, base.interfaces ?? [], type.symbol.valueDeclaration, checkNoIntersection);
}
};
this._deferUntilTypesAvailable(fqn, jsiiType.interfaces ?? [], type.symbol.valueDeclaration, checkNoIntersection);
return _sortMembers(jsiiType);
}
_visitMethod(symbol, type, ctx, declaringTypeDecl) {
if (LOG.isTraceEnabled()) {
LOG.trace(`Processing method: ${chalk.green(type.fqn)}#${chalk.cyan(symbol.name)}`);
}
const declaration = symbol.valueDeclaration;
const signature = this._typeChecker.getSignatureFromDeclaration(declaration);
if (!signature) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9004_UNABLE_TO_COMPUTE_SIGNATURE.create(declaration, symbol.name, type));
return;
}
if (type.name === Case.pascal(symbol.name)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5019_MEMBER_TYPE_NAME_CONFLICT.create(declaration.name, 'method', symbol, type).addRelatedInformationIf(declaringTypeDecl?.name ?? declaringTypeDecl, `The declaring ${type.kind} is introduced here`));
}
if (isProhibitedMemberName(symbol.name)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5016_PROHIBITED_MEMBER_NAME.create(declaration.name, symbol.name));
return;
}
this._warnAboutReservedWords(symbol);
const parameters = signature.getParameters().map((p) => this._toParameter(p, ctx));
const returnType = signature.getReturnType();
const method = bindings.setMethodRelatedNode({
abstract: _isAbstract(symbol, type) || undefined,
name: symbol.name,
parameters: parameters.length > 0 ? parameters : undefined,
protected: _isProtected(symbol) || undefined,
returns: _isVoid(returnType)
? undefined
: this._optionalValue(returnType, declaration.type ?? declaration.name, 'return type'),
async: _isPromise(returnType) || undefined,
static: _isStatic(symbol) || undefined,
locationInModule: this.declarationLocation(declaration),
}, declaration);
method.variadic = method.parameters?.some((p) => !!p.variadic) === true ? true : undefined;
this._verifyConsecutiveOptionals(declaration, method.parameters);
method.docs = this._visitDocumentation(symbol, ctx).docs;
// If the last parameter is a datatype, verify that it does not share any field names with
// other function arguments, so that it can be turned into keyword arguments by jsii frontends
// that support such.
const lastParamTypeRef = apply(last(parameters), (x) => x.type);
const lastParamSymbol = last(signature.getParameters());
if (lastParamTypeRef && spec.isNamedTypeReference(lastParamTypeRef)) {
this._deferUntilTypesAvailable(symbol.name, [lastParamTypeRef], lastParamSymbol.declarations?.[0], (lastParamType) => {
if (!spec.isInterfaceType(lastParamType) || !lastParamType.datatype) {
return;
}
// Liftable datatype, make sure no parameter names match any of the properties in the datatype
const propNames = this.allProperties(lastParamType);
const paramNames = new Set(parameters.slice(0, parameters.length - 1).map((x) => x.name));
const sharedNames = intersection(propNames, paramNames);
for (const badName of sharedNames) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5017_POSITIONAL_KEYWORD_CONFLICT.create(declaration, badName));
}
});
}
this._validateReferencedDocParams(method, symbol);
type.methods = type.methods ?? [];
if (type.methods.find((m) => m.name === method.name && m.static === method.static) != null) {
LOG.trace(`Dropping re-declaration of ${chalk.green(type.fqn)}#${chalk.cyan(method.name)}`);
return;
}
type.methods.push(method);
}
_warnAboutReservedWords(symbol) {
if (!warnings_1.enabledWarnings['reserved-word']) {
return;
}
const reservingLanguages = (0, reserved_words_1.isReservedName)(symbol.name);
if (reservingLanguages) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5018_RESERVED_WORD.create(_nameOrDeclarationNode(symbol), symbol.name, reservingLanguages));
}
}
_visitProperty(symbol, type, ctx, declaringTypeDecl) {
if (type.properties?.find((p) => p.name === symbol.name)) {
/*
* Second declaration of the same property. For example, if code specifies a getter & setter signature,
* there will be one pass for each of the signatures, but we can process only the first encountered. The
* typescript compiler will take care of making sure we don't have conflicting declarations, anyway.
*/
return;
}
if (LOG.isTraceEnabled()) {
LOG.trace(`Processing property: ${chalk.green(type.fqn)}#${chalk.cyan(symbol.name)}`);
}
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
const signature = declaration;
if (type.name === Case.pascal(symbol.name)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5019_MEMBER_TYPE_NAME_CONFLICT.create(signature.name, 'property', symbol, type).addRelatedInformationIf(declaringTypeDecl?.name ?? declaringTypeDecl, `The declaring ${type.kind} is introduced here`));
}
if (isProhibitedMemberName(symbol.name)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5016_PROHIBITED_MEMBER_NAME.create(symbol.valueDeclaration ?? symbol.declarations?.[0], symbol.name));
return;
}
this._warnAboutReservedWords(symbol);
const property = bindings.setPropertyRelatedNode({
...this._optionalValue(this._typeChecker.getTypeOfSymbolAtLocation(symbol, signature), signature.type ?? signature.name, 'property type'),
abstract: _isAbstract(symbol, type) || undefined,
name: symbol.name,
protected: _isProtected(symbol) || undefined,
static: _isStatic(symbol) || undefined,
locationInModule: this.declarationLocation(signature),
}, signature);
if (ts.isGetAccessor(signature)) {
property.immutable = true;
for (const decl of symbol.getDeclarations() ?? []) {
if (!ts.isSetAccessor(decl)) {
continue;
}
delete property.immutable;
// Verify the setter doesn't have a Separate Write Type (SWT)
const valueParam = decl.parameters[0];
if (valueParam?.type == null) {
// If there is no type node, there can't be a SWT
continue;
}
const paramType = this._typeChecker.getTypeFromTypeNode(valueParam.type);
const paramOptionalValue = this._optionalValue(paramType, valueParam.type, 'parameter type');
if (property.optional !== paramOptionalValue.optional ||
(0, spec_1.describeTypeReference)(property.type) !== (0, spec_1.describeTypeReference)(paramOptionalValue.type)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1005_SEPARATE_WRITE_TYPE.create(valueParam.type).addRelatedInformation(signature.type ?? signature.name, 'The getter signature is declared here'));
}
}
}
else {
property.immutable = (ts.getCombinedModifierFlags(signature) & ts.ModifierFlags.Readonly) !== 0 || undefined;
}
if (signature.questionToken) {
property.optional = true;
}
if (property.static && property.immutable && ts.isPropertyDeclaration(signature) && signature.initializer) {
property.const = true;
}
property.docs = this._visitDocumentation(symbol, ctx).docs;
type.properties = type.properties ?? [];
if (type.properties.find((prop) => prop.name === property.name && prop.static === property.static) != null) {
LOG.trace(`Dropping re-declaration of ${chalk.green(type.fqn)}#${chalk.cyan(property.name)}`);
return;
}
type.properties.push(property);
}
_toParameter(paramSymbol, ctx) {
if (LOG.isTraceEnabled()) {
LOG.trace(`Processing parameter: ${chalk.cyan(paramSymbol.name)}`);
}
const paramDeclaration = paramSymbol.valueDeclaration;
this._warnAboutReservedWords(paramSymbol);
const parameter = bindings.setParameterRelatedNode({
...this._optionalValue(this._typeChecker.getTypeAtLocation(paramDeclaration), paramDeclaration.type ?? paramDeclaration.name, 'parameter type'),
name: paramSymbol.name,
variadic: paramDeclaration.dotDotDotToken && true,
}, paramDeclaration);
if (parameter.variadic && spec.isCollectionTypeReference(parameter.type)) {
// TypeScript types variadic parameters as an array, but JSII uses the item-type instead.
parameter.type = parameter.type.collection.elementtype;
}
else if (paramDeclaration.initializer || paramDeclaration.questionToken) {
// Optional parameters have an inherently null-able type.
parameter.optional = true;
}
parameter.docs = this._visitDocumentation(paramSymbol, ctx.removeStability()).docs;
// Don't rewrite doc comment here on purpose -- instead, we add them as '@param'
// into the parent's doc comment.
return parameter;
}
_typeReference(type, declaration, purpose) {
const optionalValue = this._optionalValue(type, declaration, purpose);
if (optionalValue.optional) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3999_INCOHERENT_TYPE_MODEL.create(declaration, 'Encountered optional value in location where a plain type reference is expected'));
}
return optionalValue.type;
}
_optionalValue(type, declaration, purpose) {
const isThisType = _isThisType(type, this._typeChecker, declaration?.parent);
if (type.isLiteral() && _isEnumLike(type)) {
type = this._typeChecker.getBaseTypeOfLiteralType(type);
}
else {
type = this._typeChecker.getApparentType(type);
}
const primitiveType = _tryMakePrimitiveType.call(this);
if (primitiveType) {
return { type: primitiveType };
}
if (type.isUnion() && !_isEnumLike(type)) {
return _unionType.call(this);
}
if (!type.symbol) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1001_TYPE_HAS_NO_SYMBOL.create(declaration));
return { type: spec.CANONICAL_ANY };
}
if (type.symbol.name === 'Array') {
return { type: _arrayType.call(this) };
}
if (type.symbol.name === '__type' && type.symbol.members) {
return { type: _mapType.call(this) };
}
if (type.symbol.escapedName === 'Promise') {
const typeRef = type;
if (!typeRef.typeArguments || typeRef.typeArguments.length !== 1) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1002_UNSPECIFIED_PROMISE.create(declaration));
return { type: spec.CANONICAL_ANY };
}
return {
type: this._typeReference(typeRef.typeArguments[0], declaration, purpose),
};
}
const fqn = this._getFQN(type, declaration, purpose, isThisType);
if (fqn == null) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9997_UNKNOWN_ERROR.create(declaration, new Error('Could not determine FQN')));
return { type: { fqn: '' } };
}
return {
type: { fqn },
};
function _arrayType() {
const typeRef = type;
let elementtype;
if (typeRef.typeArguments?.length === 1) {
elementtype = this._typeReference(typeRef.typeArguments[0], declaration, 'list element type');
}
else {
const count = typeRef.typeArguments ? typeRef.typeArguments.length : 'none';
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1003_UNSUPPORTED_TYPE.create(declaration, `Array references must have exactly one type argument (found ${count})`));
elementtype = spec.CANONICAL_ANY;
}
return {
collection: {
elementtype,
kind: spec.CollectionKind.Array,
},
};
}
function _mapType() {
let elementtype;
const objectType = type.getStringIndexType();
if (objectType) {
elementtype = this._typeReference(objectType, declaration, 'map element type');
}
else {
const typeDecl = type.symbol.declarations?.[0];
if (typeDecl != null &&
ts.isTypeLiteralNode(typeDecl) &&
typeDecl.members.length == 1 &&
ts.isIndexSignatureDeclaration(typeDecl.members[0]) &&
typeDecl.members[0].parameters[0].type != null &&
ts.isTemplateLiteralTypeNode(typeDecl.members[0].parameters[0].type)) {
const indexTypeNode = typeDecl.members[0].type;
const indexType = this._typeChecker.getTypeFromTypeNode(indexTypeNode);
elementtype = this._typeReference(indexType, indexTypeNode, 'map element type');
}
else {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1003_UNSUPPORTED_TYPE.create(declaration, 'Only string-indexed map types are supported'));
elementtype = spec.CANONICAL_ANY;
}
}
return {
collection: {
elementtype,
kind: spec.CollectionKind.Map,
},
};
}
function _tryMakePrimitiveType() {
if (!type.symbol) {
if (type.flags & ts.TypeFlags.Object) {
if (isTupleType(type)) {
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1999_UNSUPPORTED.create(declaration, { what: 'Tuple types', alternative: 'arrays' }));
}
return { primitive: spec.PrimitiveType.Json };
}
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
return spec.CANONICAL_ANY;
}
}
else if (type.symbol.valueDeclaration &&
isUnder(type.symbol.valueDeclaration.getSourceFile().fileName, this.stdlib)) {
switch (type.symbol.name) {
case 'Boolean':
return { primitive: spec.PrimitiveType.Boolean };
case 'Date':
return { primitive: spec.PrimitiveType.Date };
case 'Number':
return { primitive: spec.PrimitiveType.Number };
case 'String':
return { primitive: spec.PrimitiveType.String };
}
}
// Not a primitive type!
return undefined;
}
function _unionType() {
const types = new Array();
let optional;
for (const subType of type.types) {
if (subType.flags & ts.TypeFlags.Undefined) {
optional = true;
continue;
}
// eslint-disable-next-line no-await-in-loop
const resolvedType = this._typeReference(subType, declaration, purpose);
if (types.some((ref) => deepEqual(ref, resolvedType))) {
continue;
}
types.push(resolvedType);
}
return types.length === 1 ? { optional, type: types[0] } : { optional, type: { union: { types } } };
}
}
callDeferredsInOrder() {
// Do a topological call order of all deferreds.
while (this._deferred.length > 0) {
// All fqns in dependency lists that don't have any pending
// deferreds themselves can be executed now, so are removed from
// dependency lists.
const pendingFqns = new Set(this._deferred.map((x) => x.fqn));
for (const deferred of this._deferred) {
restrictDependenciesTo(deferred, pendingFqns);
}
// Invoke all deferreds with no more dependencies and remove them from the list.
let invoked = false;
for (let i = 0; i < this._deferred.length; i++) {
if (this._deferred[i].dependedFqns.length === 0) {
const deferred = this._deferred.splice(i, 1)[0];
deferred.cb();
invoked = true;
}
}
if (!invoked) {
// Apparently we're stuck. Complain loudly.
throw new Error(`Could not invoke any more deferreds, cyclic dependency? Remaining: ${JSON.stringify(this._deferred, undefined, 2)}`);
}
}
/**
* Retain only elements in the dependencyfqn that are also in the set
*/
function restrictDependenciesTo(def, fqns) {
def.dependedFqns = def.dependedFqns.filter(fqns.has.bind(fqns));
}
}
/**
* Return the set of all (inherited) properties of an interface
*/
allProperties(root) {
const ret = new Set();
recurse.call(this, root);
return ret;
function recurse(int) {
for (const property of int.properties ?? []) {
ret.add(property.name);
}
for (const baseRef of int.interfaces ?? []) {
const base = this._dereference(baseRef, undefined);
if (!base) {
throw new Error('Impossible to have unresolvable base in allProperties()');
}
if (!spec.isInterfaceType(base)) {
throw new Error('Impossible to have non-interface base in allProperties()');
}
recurse.call(this, base);
}
}
}
_verifyConsecutiveOptionals(node, parameters) {
if (!parameters) {
return;
}
const remaining = [...parameters].reverse();
while (remaining.length > 0) {
const current = remaining.pop();
if (current.optional) {
const offender = remaining.find((p) => !p.optional && !p.variadic);
if (offender == null) {
continue;
}
this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3009_OPTIONAL_PARAMETER_BEFORE_REQUIRED.create(node, current, offender));
delete current.optional;
}
}
}
/**
* Updates the runtime type info with the fully-qualified name for the current class definition.
* Used by the runtime type info injector to add this information to the compiled file.
*/
registerExportedClassFqn(clazz, fqn) {
this.runtimeTypeInfoInjector.registerClassFqn(clazz, fqn);
}
/**
* Return only those submodules from the submodules list that are submodules inside this
* assembly.
*/
mySubmodules() {
return Array.from(this._submodules.values()).filter((m) => m.fqn.startsWith(`${this.projectInfo.name}.`));
}
findPackageInfo(fromDir) {
if (this._packageInfoCache.has(fromDir)) {
return this._packageInfoCache.get(fromDir);
}
const packageInfo = _findPackageInfo.call(this, fromDir);
this._packageInfoCache.set(fromDir, packageInfo);
return packageInfo;
function _findPackageInfo(dir) {
const filePath = path.join(dir, 'package.json');
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
const parent = path.dirname(dir);
if (parent === dir) {
return undefined;
}
return this.findPackageInfo(parent);
}
}
}
exports.Assembler = Assembler;
function _fingerprint(assembly) {
delete assembly.fingerprint;
assembly = sortJson(assembly);
const fingerprint = crypto.createHash('sha256').update(JSON.stringify(assembly)).digest('base64');
return { ...assembly, fingerprint };
}
function _isAbstract(symbol, declaringType) {
// everything is abstract in interfaces
if (declaringType.kind === spec.TypeKind.Interface) {
return true;
}
return (!!symbol.valueDeclaration &&
(ts.getCombinedModifierFlags(symbol.valueDeclaration) & ts.ModifierFlags.Abstract) !== 0);
}
function _isEnumLike(type) {
return (type.flags & ts.TypeFlags.EnumLike) !== 0;
}
function _isExported(node) {
return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0;
}
/**
* Members with names starting with `_` (and marked as @internal) and members
* that are private are hidden.
*
* @param symbol the symbol which should be assessed
*
* @return `true` if the symbol should be hidden
*/
function _isPrivate(symbol) {
// Private identifiers are always private...
if (symbol.name.startsWith('#')) {
return true;
}
const TYPE_DECLARATION_KINDS = new Set([
ts.SyntaxKind.ClassDeclaration,
ts.SyntaxKind.InterfaceDeclaration,
ts.SyntaxKind.EnumDeclaration,
]);
// if the symbol doesn't have a value declaration, we are assuming it's a type (enum/interface/class)
// and check that it has an "export" modifier
if (!isInternalSymbol(symbol) &&
(!symbol.valueDeclaration || TYPE_DECLARATION_KINDS.has(symbol.valueDeclaration.kind))) {
let hasExport = false;
for (const decl of symbol.declarations ?? []) {
if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) {
hasExport = true;
break;
}
// Handle nested classes from project references
if (ts.isModuleBlock(decl.parent)) {
const moduleDeclaration = decl.parent.parent;
const modifiers = ts.getCombinedModifierFlags(moduleDeclaration);
// The trick is the module is declared as ambient & exported
if ((modifiers & ts.ModifierFlags.Ambient) !== 0 && (modifiers & ts.ModifierFlags.Export) !== 0) {
hasExport = true;
break;
}
}
}
return !hasExport;
}
const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
return decl != null && (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Private) !== 0;
}
function _hasInternalJsDocTag(symbol) {
return symbol.getJsDocTags().some((t) => t.name === 'internal');
}
function _isProtected(symbol) {
return (!!symbol.valueDeclaration &&
(ts.getCombinedModifierFlags(symbol.valueDeclaration) & ts.ModifierFlags.Protected) !== 0);
}
function _isStatic(symbol) {
return (!!symbol.valueDeclaration && (ts.getCombinedModifierFlags(symbol.valueDeclaration) & ts.ModifierFlags.Static) !== 0);
}
/**
* Determines whether a given type is void or Promise<void>.
*
* @param type the tested type
*
* @returns `true` if the type is void or Promise<void>
*/
function _isVoid(type) {
if (_isPromise(type)) {
const typeRef = type;
return typeRef.typeArguments != null && typeRef.typeArguments.length === 1 && _isVoid(typeRef.typeArguments[0]);
}
return (type.flags & ts.TypeFlags.Void) !== 0;
}
function _isPromise(type) {
return type.symbol?.escapedName === 'Promise';
}
function _sortMembers(type) {
type.methods = type.methods && _sort(type.methods);
type.properties = type.properties && _sort(type.properties);
return type;
/**
* Sorts a member array such that:
* 1. Static members appear first
* 2. Immutable members appear first
* 3. Non-optional members appear first
* 4. Members appear in lexicographical order
*
* @param values the array of members to be sorted
*
* @return a sorted copy of ``values``
*/
function _sort(values) {
if (!values) {
return values;
}
return values.sort(_comparator);
function _comparator(lval, rval) {
return _format(lval).localeCompare(_format(rval));
function _format(val) {
return [val.static ? '0' : '1', val.immutable ? '0' : '1', !val.optional ? '0' : '1', val.name].join('|');
}
}
}
}
/**
* Return the last element from a list
*/
function last(xs) {
return xs.length > 0 ? xs[xs.length - 1] : undefined;
}
/**
* Apply a function to a value if it's not equal to undefined
*/
function apply(x, fn) {
return x !== undefined ? fn(x) : undefined;
}
/**
* Return the intersection of two sets
*/
function intersection(xs, ys) {
const ret = new Set();
for (const x of xs) {
if (ys.has(x)) {
ret.add(x);
}
}
return ret;
}
/**
* Return all members names of a JSII interface type
*
* Returns empty string for a non-interface type.
*/
function memberNames(jsiiType) {
return Object.keys(typeMembers(jsiiType)).filter((n) => n !== '');
}
function typeMembers(jsiiType) {
const ret = {};
for (const prop of jsiiType.properties ?? []) {
ret[prop.name] = prop;
}
for (const method of jsiiType.methods ?? []) {
ret[method.name ?? ''] = method;
}
return ret;
}
/**
* Whether or not the given name is conventionally an interface name
*
* It's an interface name if it starts with I and has another capital
* (so we don't mark IonicColumnProps as an interface).
*/
function isInterfaceName(name) {
return name.length >= 2 && name.startsWith('I') && name.charAt(1).toUpperCase() === name.charAt(1);
}
function getConstructor(type) {
return type.symbol.members?.get(ts.InternalSymbolName.Constructor);
}
function* intersect(xs, ys) {
for (const x of xs) {
if (ys.has(x)) {
yield x;
}
}
}
function noEmptyDict(xs) {
if (xs == null || Object.keys(xs).length === 0) {
return undefined;
}
return xs;
}
function toDependencyClosure(assemblies) {
const result = {};
for (const assembly of assemblies) {
if (!assembly.targets) {
continue;
}
result[assembly.name] = {
submodules: cleanUp(assembly.submodules),
targets: assembly.targets,
};
}
return result;
/**
* Removes unneeded fields from the entries part of the `dependencyClosure`
* property. Fields such as `readme` are not necessary and can bloat up the
* assembly object.
*
* This removes the `readme` and `locationInModule` fields from the submodule
* descriptios if present.
*
* @param submodules the submodules list to clean up.
*
* @returns the cleaned up submodules list.
*/
function cleanUp(submodules) {
if (submodules == null) {
return submodules;
}
const clean = {};
for (const [fqn, { targets }] of Object.entries(submodules)) {
clean[fqn] = { targets };
}
return clean;
}
}
function toSubmoduleDeclarations(submodules) {
const result = {};
for (const submodule of submodules) {
result[submodule.fqn] = {
locationInModule: submodule.locationInModule,
targets: submodule.targets,
readme: submodule.readme,
symbolId: submodule.symbolId,
};
}
return result;
}
/**
* Check whether this type is the intrinsic TypeScript "error type"
*
* This type is returned if type lookup fails. Unfortunately no public
* accessors for it are exposed.
*/
function isErrorType(t) {
return t.intrinsicName === 'error';
}
/**
* Those have specific semantics in certain languages that don't always translate cleanly in others
* (like how equals/hashCode are not a thing in Javascript, but carry meaning in Java and C#). The
* `build` name is reserved for generated code (Java builders use that).
*/
const PROHIBITED_MEMBER_NAMES = ['build', 'equals', 'hashcode'];
/**
* Whether the given name is prohibited
*/
function isProhibitedMemberName(name) {
return PROHIBITED_MEMBER_NAMES.includes(name.toLowerCase());
}
/**
* Information about the context in which a declaration is emitted.
*/
class EmitContext {
constructor(namespace, stability) {
this.namespace = namespace;
this.stability = stability;
}
/**
* Create a new EmitContext by appending a namespace entry at the end.
* @param element the new namespace entry.
*/
appendNamespace(element) {
return new EmitContext([...this.namespace, element], this.stability);
}
/**
* Create a new EmitContext by replacing the stability.
* @param stability the new stability, if available.
*/
replaceStability(stability) {
if (!stability) {
return this;
}
return new EmitContext(this.namespace, stability);
}
/**
* Create a new EmitContext without stability.
*/
removeStability() {
return new EmitContext(this.namespace, undefined);
}
}
function inferRootDir(program) {
const directories = program
.getRootFileNames()
.filter((fileName) => {
const sourceFile = program.getSourceFile(fileName);
return (sourceFile != null &&
!program.isSourceFileFromExternalLibrary(sourceFile) &&
!program.isSourceFileDefaultLibrary(sourceFile));
})
.map((fileName) => path.relative(program.getCurrentDirectory(), path.dirname(fileName)))
.map(segmentPath);
const maxPrefix = Math.min(...directories.map((segments) => segments.length - 1));
let commonIndex = -1;
while (commonIndex < maxPrefix && new Set(directories.map((segments) => segments[commonIndex + 1])).size === 1) {
commonIndex++;
}
if (commonIndex < 0) {
return undefined;
}
return directories[0][commonIndex];
function segmentPath(fileName) {
const result = new Array();
for (let parent = fileName; parent !== path.dirname(parent); parent = path.dirname(parent)) {
result.unshift(parent);
}
return result;
}
}
/**
* Determines whether the provided type is a single-valued enum. It is necessary
* to check as enums are union-like in the type model, and single-valued enum
* types are actually reduced to the only available literal, which can trip
* the assembler.
*
* @param type the type being checked.
* @param typeChecker the type checker to use to get more information.
*
* @return `true` if `type` is a single-valued enum type.
*/
function isSingleValuedEnum(type, typeChecker) {
if (type.isLiteral() && _isEnumLike(type)) {
// Single-Valued enums are reduced to the only literal available.
return type === typeChecker.getBaseTypeOfLiteralType(type);
}
return false;
}
/**
* Checks is the provided type is "this" (as a type annotation).
*
* @param type the validated type.
* @param typeChecker the type checker.
*
* @returns `true` iif the type is `this`
*/
function _isThisType(type, typeChecker, enclosingDeclaration) {
return (typeChecker.typeToTypeNode(type, enclosingDeclaration, ts.NodeBuilderFlags.None)?.kind === ts.SyntaxKind.ThisKeyword);
}
/**
* Gets the name node for a given symbol; or it's first declaration if no name can be found. This is
* intended for use in placing problem markers on the right location.
*
* @param symbol the symbol for which the name node is needed.
*
* @returns the name node for the symbol, or the symbol's first declaration.
*/
function _nameOrDeclarationNode(symbol) {
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
if (declaration == null) {
return undefined;
}
return ts.getNameOfDeclaration(declaration) ?? declaration;
}
function _findHint(decl, hint) {
const [node] = ts.getAllJSDocTags(decl, (tag) => tag.tagName.text === hint);
return node;
}
/**
* Resolve a Type to Symbol, taking into account single-valued enums which have a bug
*
* Bug reference: https://github.com/microsoft/TypeScript/issues/46755
*/
function symbolFromType(type, typeChecker) {
if ((type.flags & ts.TypeFlags.EnumLiteral) === 0) {
return type.symbol;
}
const decl = type.symbol.declarations?.[0];
if (!decl) {
return type.symbol;
}
if (!ts.isEnumMember(decl)) {
return type.symbol;
}
const parentDecl = decl.parent;
if (!parentDecl || !ts.isEnumDeclaration(parentDecl)) {
return type.symbol;
}
const name = ts.getNameOfDeclaration(parentDecl);
if (!name) {
return type.symbol;
}
return typeChecker.getSymbolAtLocation(name) ?? type.symbol;
}
const SYMBOLID_CACHE = new WeakMap();
/**
* Build and return an index of { symbolId -> fqn }
*
* Uses a cache for performance reasons.
*/
function symbolIdIndex(asm) {
const existing = SYMBOLID_CACHE.get(asm);
if (existing) {
return existing;
}
const ret = buildIndex();
SYMBOLID_CACHE.set(asm, ret);
return ret;
function buildIndex() {
const index = {};
for (const [fqn, type] of Object.entries(asm.types ?? {})) {
if (type.symbolId) {
index[type.symbolId] = fqn;
}
}
return index;
}
}
function getSymbolFromDeclaration(decl, typeChecker) {
const name = ts.getNameOfDeclaration(decl);
return name ? typeChecker.getSymbolAtLocation(name) : undefined;
}
function isTupleType(type) {
if (type.objectFlags & ts.ObjectFlags.Tuple) {
return true;
}
if (type.objectFlags & ts.ObjectFlags.Reference) {
return isTupleType(type.target);
}
return false;
}
function isUnder(file, dir) {
const relative = path.relative(dir, file);
return !relative.startsWith(path.sep) && !relative.startsWith('..');
}
function loadAndRenderReadme(readmePath, projectRoot) {
if (!fs.existsSync(readmePath)) {
return undefined;
}
return {
markdown: literate
.includeAndRenderExamples(literate.loadFromFile(readmePath), literate.fileSystemLoader(path.dirname(readmePath)), projectRoot)
.join('\n'),
};
}
const INTERNAL_SYMBOLS = new Set(Object.values(ts.InternalSymbolName));
function isInternalSymbol(symbol) {
return INTERNAL_SYMBOLS.has(symbol.name);
}
//# sourceMappingURL=assembler.js.map